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