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.ade.configuration.formatters; 029 030import org.opencms.ade.configuration.CmsConfigurationReader; 031import org.opencms.ade.configuration.I_CmsGlobalConfigurationCache; 032import org.opencms.db.CmsPublishedResource; 033import org.opencms.file.CmsFile; 034import org.opencms.file.CmsObject; 035import org.opencms.file.CmsResource; 036import org.opencms.file.CmsResourceFilter; 037import org.opencms.file.types.CmsResourceTypeFunctionConfig; 038import org.opencms.file.types.I_CmsResourceType; 039import org.opencms.loader.CmsResourceManager; 040import org.opencms.main.CmsException; 041import org.opencms.main.CmsLog; 042import org.opencms.main.OpenCms; 043import org.opencms.util.CmsStringUtil; 044import org.opencms.util.CmsUUID; 045import org.opencms.util.CmsWaitHandle; 046import org.opencms.xml.containerpage.I_CmsFormatterBean; 047import org.opencms.xml.content.CmsXmlContent; 048import org.opencms.xml.content.CmsXmlContentFactory; 049import org.opencms.xml.content.CmsXmlContentProperty; 050import org.opencms.xml.content.CmsXmlContentRootLocation; 051import org.opencms.xml.content.I_CmsXmlContentValueLocation; 052 053import java.util.ArrayList; 054import java.util.Collections; 055import java.util.HashMap; 056import java.util.HashSet; 057import java.util.List; 058import java.util.Locale; 059import java.util.Map; 060import java.util.Set; 061import java.util.concurrent.LinkedBlockingQueue; 062import java.util.concurrent.ScheduledFuture; 063import java.util.concurrent.TimeUnit; 064 065import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 066import org.apache.commons.lang3.builder.ToStringStyle; 067import org.apache.commons.logging.Log; 068 069import com.google.common.collect.Maps; 070 071/** 072 * A cache object which holds a collection of formatter configuration beans read from the VFS.<p> 073 * 074 * This class does not immediately update the cached formatter collection when changes in the VFS occur, but instead 075 * schedules an update action with a slight delay, so that if many formatters are changed in a short time, only one update 076 * operation is needed.<p> 077 * 078 * Two instances of this cache are needed, one for the Online project and one for Offline projects.<p> 079 **/ 080public class CmsFormatterConfigurationCache implements I_CmsGlobalConfigurationCache { 081 082 /** Node name for the FormatterKey node. */ 083 public static final String N_FORMATTER_KEY = "FormatterKey"; 084 085 /** A UUID which is used to mark the configuration cache for complete reloading. */ 086 public static final CmsUUID RELOAD_MARKER = CmsUUID.getNullUUID(); 087 088 /** The resource type for macro formatters. */ 089 public static final String TYPE_FLEX_FORMATTER = "flex_formatter"; 090 091 /** The resource type for formatter configurations. */ 092 public static final String TYPE_FORMATTER_CONFIG = "formatter_config"; 093 094 /** The resource type for macro formatters. */ 095 public static final String TYPE_MACRO_FORMATTER = "macro_formatter"; 096 097 /** Type name for setting configurations. */ 098 public static final String TYPE_SETTINGS_CONFIG = "settings_config"; 099 100 /** The delay to use for updating the formatter cache, in seconds. */ 101 protected static int UPDATE_DELAY_MILLIS = 500; 102 103 /** The logger for this class. */ 104 private static final Log LOG = CmsLog.getLog(CmsFormatterConfigurationCache.class); 105 106 /** The CMS context used by this cache. */ 107 private CmsObject m_cms; 108 109 /** The cache name. */ 110 private String m_name; 111 112 /** Additional setting configurations. */ 113 private volatile Map<CmsUUID, Map<CmsSharedSettingKey, CmsXmlContentProperty>> m_settingConfigs; 114 115 /** The current data contained in the formatter cache.<p> This field is reassigned when formatters are changed, but the objects pointed to by this field are immutable.<p> **/ 116 private volatile CmsFormatterConfigurationCacheState m_state = new CmsFormatterConfigurationCacheState( 117 Collections.<CmsUUID, I_CmsFormatterBean> emptyMap()); 118 119 /** The future for the scheduled task. */ 120 private volatile ScheduledFuture<?> m_taskFuture; 121 122 /** The work queue to keep track of what needs to be done during the next cache update. */ 123 private LinkedBlockingQueue<Object> m_workQueue = new LinkedBlockingQueue<>(); 124 125 /** 126 * Creates a new formatter configuration cache instance.<p> 127 * 128 * @param cms the CMS context to use 129 * @param name the cache name 130 * 131 * @throws CmsException if something goes wrong 132 */ 133 public CmsFormatterConfigurationCache(CmsObject cms, String name) 134 throws CmsException { 135 136 m_cms = OpenCms.initCmsObject(cms); 137 Map<CmsUUID, I_CmsFormatterBean> noFormatters = Collections.emptyMap(); 138 m_state = new CmsFormatterConfigurationCacheState(noFormatters); 139 m_name = name; 140 } 141 142 /** 143 * Adds a wait handle to the list of wait handles.<p> 144 * 145 * @param handle the handle to add 146 */ 147 public void addWaitHandle(CmsWaitHandle handle) { 148 149 m_workQueue.add(handle); 150 } 151 152 /** 153 * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#clear() 154 */ 155 public void clear() { 156 157 markForUpdate(RELOAD_MARKER); 158 } 159 160 /** 161 * Gets the cache instance name.<p> 162 * 163 * @return the cache instance name 164 */ 165 public String getName() { 166 167 return m_name; 168 } 169 170 /** 171 * Gets the collection of cached formatters.<p> 172 * 173 * @return the collection of cached formatters 174 */ 175 public CmsFormatterConfigurationCacheState getState() { 176 177 return m_state; 178 } 179 180 /** 181 * Initializes the cache and installs the update task.<p> 182 */ 183 public void initialize() { 184 185 if (m_taskFuture != null) { 186 m_taskFuture.cancel(false); 187 m_taskFuture = null; 188 } 189 reload(); 190 m_taskFuture = OpenCms.getExecutor().scheduleWithFixedDelay( 191 this::performUpdate, 192 UPDATE_DELAY_MILLIS, 193 UPDATE_DELAY_MILLIS, 194 TimeUnit.MILLISECONDS); 195 196 } 197 198 /** 199 * The method called by the scheduled update action to update the cache.<p> 200 */ 201 public void performUpdate() { 202 203 // Wrap everything in try-catch because we don't want to leak an exception out of a scheduled task 204 try { 205 ArrayList<Object> work = new ArrayList<>(); 206 m_workQueue.drainTo(work); 207 Set<CmsUUID> copiedIds = new HashSet<CmsUUID>(); 208 List<CmsWaitHandle> waitHandles = new ArrayList<>(); 209 for (Object o : work) { 210 if (o instanceof CmsUUID) { 211 copiedIds.add((CmsUUID)o); 212 } else if (o instanceof CmsWaitHandle) { 213 waitHandles.add((CmsWaitHandle)o); 214 } 215 } 216 if (copiedIds.contains(RELOAD_MARKER)) { 217 // clear cache event, reload all formatter configurations 218 reload(); 219 } else { 220 // normal case: incremental update 221 Map<CmsUUID, I_CmsFormatterBean> formattersToUpdate = Maps.newHashMap(); 222 for (CmsUUID structureId : copiedIds) { 223 I_CmsFormatterBean formatterBean = readFormatter(structureId); 224 // formatterBean may be null here 225 formattersToUpdate.put(structureId, formatterBean); 226 } 227 m_state = m_state.createUpdatedCopy(formattersToUpdate); 228 } 229 if (copiedIds.size() > 0) { 230 OpenCms.getADEManager().getCache().flushContainerPages( 231 m_cms.getRequestContext().getCurrentProject().isOnlineProject()); 232 } 233 for (CmsWaitHandle handle : waitHandles) { 234 handle.release(); 235 } 236 } catch (Exception e) { 237 LOG.error(e.getLocalizedMessage(), e); 238 } 239 } 240 241 /** 242 * Reloads the formatter cache.<p> 243 */ 244 public void reload() { 245 246 List<CmsResource> settingConfigResources = new ArrayList<>(); 247 try { 248 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(TYPE_SETTINGS_CONFIG); 249 CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(type); 250 settingConfigResources.addAll(m_cms.readResources("/", filter)); 251 } catch (CmsException e) { 252 LOG.warn(e.getLocalizedMessage(), e); 253 } 254 255 Map<CmsUUID, Map<CmsSharedSettingKey, CmsXmlContentProperty>> sharedSettingsByStructureId = new HashMap<>(); 256 for (CmsResource resource : settingConfigResources) { 257 Map<CmsSharedSettingKey, CmsXmlContentProperty> sharedSettings = parseSettingsConfig(resource); 258 if (sharedSettings != null) { 259 sharedSettingsByStructureId.put(resource.getStructureId(), sharedSettings); 260 } 261 } 262 m_settingConfigs = sharedSettingsByStructureId; 263 264 List<CmsResource> formatterResources = new ArrayList<CmsResource>(); 265 try { 266 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(TYPE_FORMATTER_CONFIG); 267 CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(type); 268 formatterResources.addAll(m_cms.readResources("/", filter)); 269 } catch (CmsException e) { 270 LOG.warn(e.getLocalizedMessage(), e); 271 } 272 try { 273 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(TYPE_MACRO_FORMATTER); 274 CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(type); 275 formatterResources.addAll(m_cms.readResources("/", filter)); 276 I_CmsResourceType typeFlex = OpenCms.getResourceManager().getResourceType(TYPE_FLEX_FORMATTER); 277 CmsResourceFilter filterFlex = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(typeFlex); 278 formatterResources.addAll(m_cms.readResources("/", filterFlex)); 279 I_CmsResourceType typeFunction = OpenCms.getResourceManager().getResourceType( 280 CmsResourceTypeFunctionConfig.TYPE_NAME); 281 CmsResourceFilter filterFunction = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(typeFunction); 282 formatterResources.addAll(m_cms.readResources("/", filterFunction)); 283 } catch (CmsException e) { 284 LOG.warn(e.getLocalizedMessage(), e); 285 } 286 Map<CmsUUID, I_CmsFormatterBean> newFormatters = Maps.newHashMap(); 287 for (CmsResource formatterResource : formatterResources) { 288 I_CmsFormatterBean formatterBean = readFormatter(formatterResource.getStructureId()); 289 if (formatterBean != null) { 290 newFormatters.put(formatterResource.getStructureId(), formatterBean); 291 } 292 } 293 m_state = new CmsFormatterConfigurationCacheState(newFormatters); 294 295 } 296 297 /** 298 * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#remove(org.opencms.db.CmsPublishedResource) 299 */ 300 public void remove(CmsPublishedResource pubRes) { 301 302 checkIfUpdateIsNeeded(pubRes.getStructureId(), pubRes.getRootPath(), pubRes.getType()); 303 } 304 305 /** 306 * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#remove(org.opencms.file.CmsResource) 307 */ 308 public void remove(CmsResource resource) { 309 310 checkIfUpdateIsNeeded(resource.getStructureId(), resource.getRootPath(), resource.getTypeId()); 311 } 312 313 /** 314 * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#update(org.opencms.db.CmsPublishedResource) 315 */ 316 public void update(CmsPublishedResource pubRes) { 317 318 checkIfUpdateIsNeeded(pubRes.getStructureId(), pubRes.getRootPath(), pubRes.getType()); 319 } 320 321 /** 322 * @see org.opencms.ade.configuration.I_CmsGlobalConfigurationCache#update(org.opencms.file.CmsResource) 323 */ 324 public void update(CmsResource resource) { 325 326 checkIfUpdateIsNeeded(resource.getStructureId(), resource.getRootPath(), resource.getTypeId()); 327 } 328 329 /** 330 * Waits until no update action is scheduled.<p> 331 * 332 * Should only be used in tests.<p> 333 */ 334 public void waitForUpdate() { 335 336 CmsWaitHandle handle = new CmsWaitHandle(true); 337 addWaitHandle(handle); 338 handle.enter(Long.MAX_VALUE); 339 } 340 341 /** 342 * Reads a formatter given its structure id and returns it, or null if the formatter couldn't be read.<p> 343 * 344 * @param structureId the structure id of the formatter configuration 345 * 346 * @return the formatter bean, or null if no formatter could be read for some reason 347 */ 348 protected I_CmsFormatterBean readFormatter(CmsUUID structureId) { 349 350 I_CmsFormatterBean formatterBean = null; 351 CmsResource formatterRes = null; 352 try { 353 formatterRes = m_cms.readResource(structureId); 354 CmsFile formatterFile = m_cms.readFile(formatterRes); 355 CmsFormatterBeanParser parser = new CmsFormatterBeanParser(m_cms, m_settingConfigs); 356 CmsXmlContent content = CmsXmlContentFactory.unmarshal(m_cms, formatterFile); 357 formatterBean = parser.parse(content, formatterRes.getRootPath(), "" + formatterRes.getStructureId()); 358 } catch (Exception e) { 359 360 if (formatterRes == null) { 361 // normal case if resources get deleted, should not be written to the error channel 362 LOG.info("Could not read formatter with id " + structureId); 363 } else { 364 LOG.error( 365 "Error while trying to read formatter configuration " 366 + formatterRes.getRootPath() 367 + ": " 368 + e.getLocalizedMessage(), 369 e); 370 } 371 } 372 return formatterBean; 373 } 374 375 /** 376 * Checks if an update of the formatter is needed and if so, adds its structure id to the update set.<p> 377 * 378 * @param structureId the structure id of the formatter 379 * @param path the path of the formatter 380 * @param resourceType the resource type 381 */ 382 private void checkIfUpdateIsNeeded(CmsUUID structureId, String path, int resourceType) { 383 384 if (CmsResource.isTemporaryFileName(path)) { 385 return; 386 } 387 CmsResourceManager manager = OpenCms.getResourceManager(); 388 389 if (manager.matchResourceType(TYPE_SETTINGS_CONFIG, resourceType)) { 390 // for each formatter configuration, only the combined settings are stored, not 391 // the reference to the settings config. So we need to reload everything when a setting configuration 392 // changes. 393 markForUpdate(RELOAD_MARKER); 394 return; 395 } 396 397 if (manager.matchResourceType(TYPE_FORMATTER_CONFIG, resourceType) 398 || manager.matchResourceType(TYPE_MACRO_FORMATTER, resourceType) 399 || manager.matchResourceType(TYPE_FLEX_FORMATTER, resourceType) 400 || manager.matchResourceType(CmsResourceTypeFunctionConfig.TYPE_NAME, resourceType)) { 401 markForUpdate(structureId); 402 } 403 } 404 405 /** 406 * Adds a formatter structure id to the update set, and schedule an update task unless one is already scheduled.<p> 407 * 408 * @param structureId the structure id of the formatter configuration 409 */ 410 private void markForUpdate(CmsUUID structureId) { 411 412 m_workQueue.add(structureId); 413 } 414 415 /** 416 * Helper method for parsing a settings configuration file. 417 * 418 * <p> If a setting definition contains formatter keys, then one entry for each formatter key will be added to the result 419 * map, otherwise just one general map entry with formatterKey = null will be generated for that setting. 420 * 421 * @param resource the resource to parse 422 * @return the parsed setting definitions 423 */ 424 private Map<CmsSharedSettingKey, CmsXmlContentProperty> parseSettingsConfig(CmsResource resource) { 425 426 Map<CmsSharedSettingKey, CmsXmlContentProperty> result = new HashMap<>(); 427 try { 428 CmsFile settingFile = m_cms.readFile(resource); 429 CmsXmlContent settingContent = CmsXmlContentFactory.unmarshal(m_cms, settingFile); 430 CmsXmlContentRootLocation location = new CmsXmlContentRootLocation(settingContent, Locale.ENGLISH); 431 for (I_CmsXmlContentValueLocation settingLoc : location.getSubValues(CmsFormatterBeanParser.N_SETTING)) { 432 CmsXmlContentProperty setting = CmsConfigurationReader.parseProperty( 433 m_cms, 434 settingLoc).getPropertyData(); 435 String includeName = setting.getIncludeName(setting.getName()); 436 if (includeName == null) { 437 LOG.warn( 438 "No include name defined for setting in " 439 + resource.getRootPath() 440 + ", setting = " 441 + ReflectionToStringBuilder.toString(setting, ToStringStyle.SHORT_PREFIX_STYLE)); 442 continue; 443 } 444 Set<String> formatterKeys = new HashSet<>(); 445 for (I_CmsXmlContentValueLocation formatterKeyLoc : settingLoc.getSubValues(N_FORMATTER_KEY)) { 446 String formatterKey = formatterKeyLoc.getValue().getStringValue(m_cms); 447 if (!CmsStringUtil.isEmptyOrWhitespaceOnly(formatterKey)) { 448 formatterKeys.add(formatterKey.trim()); 449 } 450 } 451 if (formatterKeys.size() == 0) { 452 result.put(new CmsSharedSettingKey(includeName, null), setting); 453 } else { 454 for (String formatterKey : formatterKeys) { 455 result.put(new CmsSharedSettingKey(includeName, formatterKey), setting); 456 457 } 458 } 459 } 460 return result; 461 } catch (Exception e) { 462 LOG.error(e.getLocalizedMessage()); 463 return null; 464 } 465 } 466}