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}