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    public static 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     * Extracts the locale and base name from a resource's file name.<p>
142     *
143     * @param bundleRes the resource for which to get the base name and locale
144     * @return a bean containing the base name and locale
145     */
146    public static NameAndLocale getNameAndLocale(CmsResource bundleRes) {
147
148        String fileName = bundleRes.getName();
149        if (TYPE_PROPERTIES_BUNDLE.equals(OpenCms.getResourceManager().getResourceType(bundleRes).getTypeName())) {
150            String localeSuffix = CmsStringUtil.getLocaleSuffixForName(fileName);
151            if (localeSuffix == null) {
152                return new NameAndLocale(fileName, null);
153            } else {
154                String base = fileName.substring(
155                    0,
156                    fileName.lastIndexOf(localeSuffix) - (1 /* cut off trailing underscore, too*/));
157                Locale locale = CmsLocaleManager.getLocale(localeSuffix);
158                return new NameAndLocale(base, locale);
159            }
160        } else {
161            return new NameAndLocale(fileName, null);
162        }
163    }
164
165    /**
166     * Collects all locales possibly used in the system.<p>
167     *
168     * @return the collection of all locales
169     */
170    private static Collection<Locale> getAllLocales() {
171
172        Set<Locale> result = new HashSet<Locale>();
173        result.addAll(OpenCms.getWorkplaceManager().getLocales());
174        result.addAll(OpenCms.getLocaleManager().getAvailableLocales());
175        return result;
176    }
177
178    /**
179     * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent)
180     */
181    public void cmsEvent(CmsEvent event) {
182
183        // wrap in try-catch so that errors don't affect other handlers
184        try {
185            handleEvent(event);
186        } catch (Throwable t) {
187            LOG.error(t.getLocalizedMessage(), t);
188        }
189    }
190
191    /**
192     * Indicates if a reload thread is currently scheduled.
193     *
194     * @return <code>true</code> if a reload is currently scheduled
195     */
196    public boolean isReloadScheduled() {
197
198        return m_reloadIsScheduled;
199    }
200
201    /**
202     * Re-initializes the resource bundles.<p>
203     *
204     * @param isStartup true when this is called during startup
205     */
206    public synchronized void reload(boolean isStartup) {
207
208        if ((OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT)
209            && OpenCms.getResourceManager().hasResourceType(TYPE_XML_BUNDLE)) {
210            List<CmsResource> xmlBundles = Lists.newArrayList();
211            List<CmsResource> propertyBundles = Lists.newArrayList();
212            try {
213                I_CmsResourceType xmlType = OpenCms.getResourceManager().getResourceType(TYPE_XML_BUNDLE);
214                xmlBundles = m_cms.readResources("/", CmsResourceFilter.ALL.addRequireType(xmlType), true);
215            } catch (Exception e) {
216                logError(e, isStartup);
217            }
218            try {
219                I_CmsResourceType propType = OpenCms.getResourceManager().getResourceType(TYPE_PROPERTIES_BUNDLE);
220                propertyBundles = m_cms.readResources("/", CmsResourceFilter.ALL.addRequireType(propType), true);
221            } catch (Exception e) {
222                logError(e, isStartup);
223            }
224            try {
225
226                synchronized (CmsResourceBundleLoader.class) {
227                    CmsResourceBundleLoader.flushBundleCache();
228                    for (String baseName : m_bundleBaseNames) {
229                        CmsResourceBundleLoader.flushPermanentCache(baseName);
230                    }
231                    m_bundleBaseNames.clear();
232                    for (CmsResource xmlBundle : xmlBundles) {
233                        addXmlBundle(xmlBundle);
234                    }
235                    for (CmsResource propertyBundle : propertyBundles) {
236                        addPropertyBundle(propertyBundle);
237                    }
238                    if (OpenCms.getWorkplaceManager() != null) {
239                        OpenCms.getWorkplaceManager().flushMessageCache();
240                    }
241                }
242            } catch (Exception e) {
243                logError(e, isStartup);
244            }
245        }
246    }
247
248    /**
249     * Sets the information if a reload thread is currently scheduled.
250     *
251     * @param reloadIsScheduled if <code>true</code> there is a reload currently scheduled
252     */
253    public void setReloadScheduled(boolean reloadIsScheduled) {
254
255        m_reloadIsScheduled = reloadIsScheduled;
256    }
257
258    /**
259     * Shuts down the VFS bundle manager.<p>
260     *
261     * This will cause the internal reloading Thread not reload in case it is still running.<p>
262     */
263    public void shutDown() {
264
265        // we don't want to listen to further events
266        OpenCms.getEventManager().removeCmsEventListener(this);
267        setReloadScheduled(false);
268        if (CmsLog.INIT.isInfoEnabled()) {
269            CmsLog.INIT.info(
270                org.opencms.staticexport.Messages.get().getBundle().key(
271                    org.opencms.staticexport.Messages.INIT_SHUTDOWN_1,
272                    this.getClass().getName()));
273        }
274    }
275
276    /**
277     * Logs an exception that occurred.<p>
278     *
279     * @param e the exception to log
280     * @param logToErrorChannel if true erros should be written to the error channel instead of the info channel
281     */
282    protected void logError(Exception e, boolean logToErrorChannel) {
283
284        if (logToErrorChannel) {
285            LOG.error(e.getLocalizedMessage(), e);
286        } else {
287            LOG.info(e.getLocalizedMessage(), e);
288        }
289        // if an error was logged make sure that the flag to schedule a reload is reset
290        setReloadScheduled(false);
291    }
292
293    /**
294     * Internal method for adding a resource bundle to the internal cache.<p>
295     *
296     * @param baseName the base name of the resource bundle
297     * @param locale the locale of the resource bundle
298     * @param bundle the resource bundle to add
299     */
300    private void addBundle(String baseName, Locale locale, I_CmsResourceBundle bundle) {
301
302        CmsResourceBundleLoader.addBundleToCache(baseName, locale, bundle);
303    }
304
305    /**
306     * Adds a resource bundle based on a properties file in the VFS.<p>
307     *
308     * @param bundleResource the properties file
309     */
310    private void addPropertyBundle(CmsResource bundleResource) {
311
312        NameAndLocale nameAndLocale = getNameAndLocale(bundleResource);
313        Locale locale = nameAndLocale.getLocale();
314
315        String baseName = nameAndLocale.getName();
316        m_bundleBaseNames.add(baseName);
317        LOG.info(
318            String.format(
319                "Adding property VFS bundle (path=%s, name=%s, locale=%s)",
320                bundleResource.getRootPath(),
321                baseName,
322                "" + locale));
323        Locale paramLocale = locale != null ? locale : CmsLocaleManager.getDefaultLocale();
324        CmsVfsBundleParameters params = new CmsVfsBundleParameters(
325            nameAndLocale.getName(),
326            bundleResource.getRootPath(),
327            paramLocale,
328            locale == null,
329            CmsVfsResourceBundle.TYPE_PROPERTIES);
330        CmsVfsResourceBundle bundle = new CmsVfsResourceBundle(params);
331        addBundle(baseName, locale, bundle);
332    }
333
334    /**
335     * Adds an XML based message bundle.<p>
336     *
337     * @param xmlBundle the XML content containing the message bundle data
338     */
339    private void addXmlBundle(CmsResource xmlBundle) {
340
341        String name = xmlBundle.getName();
342        String path = xmlBundle.getRootPath();
343        m_bundleBaseNames.add(name);
344
345        LOG.info(String.format("Adding property VFS bundle (path=%s, name=%s)", xmlBundle.getRootPath(), name));
346        for (Locale locale : getAllLocales()) {
347            CmsVfsBundleParameters params = new CmsVfsBundleParameters(
348                name,
349                path,
350                locale,
351                false,
352                CmsVfsResourceBundle.TYPE_XML);
353            CmsVfsResourceBundle bundle = new CmsVfsResourceBundle(params);
354            addBundle(name, locale, bundle);
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}