001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (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.i18n; 029 030import org.opencms.db.CmsPublishedResource; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.file.types.I_CmsResourceType; 035import org.opencms.main.CmsEvent; 036import org.opencms.main.CmsException; 037import org.opencms.main.CmsLog; 038import org.opencms.main.I_CmsEventListener; 039import org.opencms.main.OpenCms; 040import org.opencms.util.CmsStringUtil; 041import org.opencms.util.CmsUUID; 042 043import java.util.Collection; 044import java.util.HashSet; 045import java.util.List; 046import java.util.Locale; 047import java.util.Set; 048 049import org.apache.commons.logging.Log; 050 051import com.google.common.collect.Lists; 052 053/** 054 * Manages message bundles loaded from the VFS.<p> 055 */ 056public class CmsVfsBundleManager implements I_CmsEventListener { 057 058 /** 059 * Data holder for a base name and locale of a message bundle.<p> 060 */ 061 private class NameAndLocale { 062 063 /** The locale. */ 064 private Locale m_locale; 065 066 /** The base name. */ 067 private String m_name; 068 069 /** 070 * Creates a new instance.<p> 071 * 072 * @param name the base name 073 * @param locale the locale 074 */ 075 public NameAndLocale(String name, Locale locale) { 076 077 m_name = name; 078 m_locale = locale; 079 } 080 081 /** 082 * Gets the locale.<p> 083 * 084 * @return the locale 085 */ 086 public Locale getLocale() { 087 088 return m_locale; 089 } 090 091 /** 092 * Gets the base name.<p> 093 * 094 * @return the base name 095 */ 096 public String getName() { 097 098 return m_name; 099 } 100 } 101 102 /** Resource type name for plain-text properties files containing messages. */ 103 public static final String TYPE_PROPERTIES_BUNDLE = "propertyvfsbundle"; 104 105 /** Resource type name for XML contents containing messages. */ 106 public static final String TYPE_XML_BUNDLE = "xmlvfsbundle"; 107 108 /** The logger instance for this class. */ 109 protected static final Log LOG = CmsLog.getLog(CmsVfsBundleManager.class); 110 111 /** The set of bundle base names. */ 112 private Set<String> m_bundleBaseNames; 113 114 /** The CMS context to use. */ 115 private CmsObject m_cms; 116 117 /** Indicated if a reload is already scheduled. */ 118 private boolean m_reloadIsScheduled; 119 120 /** Thread generation counter. */ 121 private int m_threadCount; 122 123 /** 124 * Creates a new instance.<p> 125 * 126 * @param cms the CMS context to use 127 */ 128 public CmsVfsBundleManager(CmsObject cms) { 129 130 m_cms = cms; 131 m_bundleBaseNames = new HashSet<String>(); 132 CmsVfsResourceBundle.setCmsObject(cms); 133 OpenCms.getEventManager().addCmsEventListener( 134 this, 135 new int[] {I_CmsEventListener.EVENT_PUBLISH_PROJECT, I_CmsEventListener.EVENT_CLEAR_CACHES}); 136 // immediately load all bundles for the first time 137 reload(true); 138 } 139 140 /** 141 * Collects all locales possibly used in the system.<p> 142 * 143 * @return the collection of all locales 144 */ 145 private static Collection<Locale> getAllLocales() { 146 147 Set<Locale> result = new HashSet<Locale>(); 148 result.addAll(OpenCms.getWorkplaceManager().getLocales()); 149 result.addAll(OpenCms.getLocaleManager().getAvailableLocales()); 150 return result; 151 } 152 153 /** 154 * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent) 155 */ 156 public void cmsEvent(CmsEvent event) { 157 158 // wrap in try-catch so that errors don't affect other handlers 159 try { 160 handleEvent(event); 161 } catch (Throwable t) { 162 LOG.error(t.getLocalizedMessage(), t); 163 } 164 } 165 166 /** 167 * Indicates if a reload thread is currently scheduled. 168 * 169 * @return <code>true</code> if a reload is currently scheduled 170 */ 171 public boolean isReloadScheduled() { 172 173 return m_reloadIsScheduled; 174 } 175 176 /** 177 * Re-initializes the resource bundles.<p> 178 * 179 * @param isStartup true when this is called during startup 180 */ 181 public synchronized void reload(boolean isStartup) { 182 183 if ((OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) 184 && OpenCms.getResourceManager().hasResourceType(TYPE_XML_BUNDLE)) { 185 List<CmsResource> xmlBundles = Lists.newArrayList(); 186 List<CmsResource> propertyBundles = Lists.newArrayList(); 187 try { 188 I_CmsResourceType xmlType = OpenCms.getResourceManager().getResourceType(TYPE_XML_BUNDLE); 189 xmlBundles = m_cms.readResources("/", CmsResourceFilter.ALL.addRequireType(xmlType), true); 190 } catch (Exception e) { 191 logError(e, isStartup); 192 } 193 try { 194 I_CmsResourceType propType = OpenCms.getResourceManager().getResourceType(TYPE_PROPERTIES_BUNDLE); 195 propertyBundles = m_cms.readResources("/", CmsResourceFilter.ALL.addRequireType(propType), true); 196 } catch (Exception e) { 197 logError(e, isStartup); 198 } 199 try { 200 201 synchronized (CmsResourceBundleLoader.class) { 202 CmsResourceBundleLoader.flushBundleCache(); 203 for (String baseName : m_bundleBaseNames) { 204 CmsResourceBundleLoader.flushPermanentCache(baseName); 205 } 206 m_bundleBaseNames.clear(); 207 for (CmsResource xmlBundle : xmlBundles) { 208 addXmlBundle(xmlBundle); 209 } 210 for (CmsResource propertyBundle : propertyBundles) { 211 addPropertyBundle(propertyBundle); 212 } 213 if (OpenCms.getWorkplaceManager() != null) { 214 OpenCms.getWorkplaceManager().flushMessageCache(); 215 } 216 } 217 } catch (Exception e) { 218 logError(e, isStartup); 219 } 220 } 221 } 222 223 /** 224 * Sets the information if a reload thread is currently scheduled. 225 * 226 * @param reloadIsScheduled if <code>true</code> there is a reload currently scheduled 227 */ 228 public void setReloadScheduled(boolean reloadIsScheduled) { 229 230 m_reloadIsScheduled = reloadIsScheduled; 231 } 232 233 /** 234 * Shuts down the VFS bundle manager.<p> 235 * 236 * This will cause the internal reloading Thread not reload in case it is still running.<p> 237 */ 238 public void shutDown() { 239 240 // we don't want to listen to further events 241 OpenCms.getEventManager().removeCmsEventListener(this); 242 setReloadScheduled(false); 243 if (CmsLog.INIT.isInfoEnabled()) { 244 CmsLog.INIT.info( 245 org.opencms.staticexport.Messages.get().getBundle().key( 246 org.opencms.staticexport.Messages.INIT_SHUTDOWN_1, 247 this.getClass().getName())); 248 } 249 } 250 251 /** 252 * Logs an exception that occurred.<p> 253 * 254 * @param e the exception to log 255 * @param logToErrorChannel if true erros should be written to the error channel instead of the info channel 256 */ 257 protected void logError(Exception e, boolean logToErrorChannel) { 258 259 if (logToErrorChannel) { 260 LOG.error(e.getLocalizedMessage(), e); 261 } else { 262 LOG.info(e.getLocalizedMessage(), e); 263 } 264 // if an error was logged make sure that the flag to schedule a reload is reset 265 setReloadScheduled(false); 266 } 267 268 /** 269 * Internal method for adding a resource bundle to the internal cache.<p> 270 * 271 * @param baseName the base name of the resource bundle 272 * @param locale the locale of the resource bundle 273 * @param bundle the resource bundle to add 274 */ 275 private void addBundle(String baseName, Locale locale, I_CmsResourceBundle bundle) { 276 277 CmsResourceBundleLoader.addBundleToCache(baseName, locale, bundle); 278 } 279 280 /** 281 * Adds a resource bundle based on a properties file in the VFS.<p> 282 * 283 * @param bundleResource the properties file 284 */ 285 private void addPropertyBundle(CmsResource bundleResource) { 286 287 NameAndLocale nameAndLocale = getNameAndLocale(bundleResource); 288 Locale locale = nameAndLocale.getLocale(); 289 290 String baseName = nameAndLocale.getName(); 291 m_bundleBaseNames.add(baseName); 292 LOG.info( 293 String.format( 294 "Adding property VFS bundle (path=%s, name=%s, locale=%s)", 295 bundleResource.getRootPath(), 296 baseName, 297 "" + locale)); 298 Locale paramLocale = locale != null ? locale : CmsLocaleManager.getDefaultLocale(); 299 CmsVfsBundleParameters params = new CmsVfsBundleParameters( 300 nameAndLocale.getName(), 301 bundleResource.getRootPath(), 302 paramLocale, 303 locale == null, 304 CmsVfsResourceBundle.TYPE_PROPERTIES); 305 CmsVfsResourceBundle bundle = new CmsVfsResourceBundle(params); 306 addBundle(baseName, locale, bundle); 307 } 308 309 /** 310 * Adds an XML based message bundle.<p> 311 * 312 * @param xmlBundle the XML content containing the message bundle data 313 */ 314 private void addXmlBundle(CmsResource xmlBundle) { 315 316 String name = xmlBundle.getName(); 317 String path = xmlBundle.getRootPath(); 318 m_bundleBaseNames.add(name); 319 320 LOG.info(String.format("Adding property VFS bundle (path=%s, name=%s)", xmlBundle.getRootPath(), name)); 321 for (Locale locale : getAllLocales()) { 322 CmsVfsBundleParameters params = new CmsVfsBundleParameters( 323 name, 324 path, 325 locale, 326 false, 327 CmsVfsResourceBundle.TYPE_XML); 328 CmsVfsResourceBundle bundle = new CmsVfsResourceBundle(params); 329 addBundle(name, locale, bundle); 330 } 331 } 332 333 /** 334 * Extracts the locale and base name from a resource's file name.<p> 335 * 336 * @param bundleRes the resource for which to get the base name and locale 337 * @return a bean containing the base name and locale 338 */ 339 private NameAndLocale getNameAndLocale(CmsResource bundleRes) { 340 341 String fileName = bundleRes.getName(); 342 if (TYPE_PROPERTIES_BUNDLE.equals(OpenCms.getResourceManager().getResourceType(bundleRes).getTypeName())) { 343 String localeSuffix = CmsStringUtil.getLocaleSuffixForName(fileName); 344 if (localeSuffix == null) { 345 return new NameAndLocale(fileName, null); 346 } else { 347 String base = fileName.substring( 348 0, 349 fileName.lastIndexOf(localeSuffix) - (1 /* cut off trailing underscore, too*/)); 350 Locale locale = CmsLocaleManager.getLocale(localeSuffix); 351 return new NameAndLocale(base, locale); 352 } 353 } else { 354 return new NameAndLocale(fileName, null); 355 } 356 } 357 358 /** 359 * This actually handles the event.<p> 360 * 361 * @param event the received event 362 */ 363 private void handleEvent(CmsEvent event) { 364 365 switch (event.getType()) { 366 case I_CmsEventListener.EVENT_PUBLISH_PROJECT: 367 //System.out.print(getEventName(event.getType())); 368 String publishIdStr = (String)event.getData().get(I_CmsEventListener.KEY_PUBLISHID); 369 if (publishIdStr != null) { 370 CmsUUID publishId = new CmsUUID(publishIdStr); 371 try { 372 List<CmsPublishedResource> publishedResources = m_cms.readPublishedResources(publishId); 373 if (!publishedResources.isEmpty()) { 374 String[] typesToMatch = new String[] {TYPE_PROPERTIES_BUNDLE, TYPE_XML_BUNDLE}; 375 boolean reload = false; 376 for (CmsPublishedResource res : publishedResources) { 377 for (String typeName : typesToMatch) { 378 if (OpenCms.getResourceManager().matchResourceType(typeName, res.getType())) { 379 reload = true; 380 break; 381 } 382 } 383 } 384 if (reload) { 385 scheduleReload(); 386 } 387 } 388 } catch (CmsException e) { 389 LOG.error(e.getLocalizedMessage(), e); 390 } 391 } 392 break; 393 case I_CmsEventListener.EVENT_CLEAR_CACHES: 394 scheduleReload(); 395 break; 396 default: 397 } 398 } 399 400 /** 401 * Schedules a bundle reload.<p> 402 */ 403 private void scheduleReload() { 404 405 if (!isReloadScheduled() && (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT)) { 406 // only schedule a reload if the system is not going down already 407 m_threadCount++; 408 Thread thread = new Thread("Bundle reload Thread " + m_threadCount) { 409 410 @Override 411 public void run() { 412 413 setReloadScheduled(true); 414 try { 415 Thread.sleep(1000); 416 } catch (Exception e) { 417 // ignore 418 } 419 if (isReloadScheduled()) { 420 reload(false); 421 } 422 setReloadScheduled(false); 423 } 424 }; 425 thread.start(); 426 } 427 } 428}