/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.web; import java.io.IOException; import java.io.StringReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.localization.ContextualLocalizationManager; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; /** * Internationalization service based on key/property values. The key is the id of the message being looked for and the * returned value is the message in the language requested. There are 3 sources where properties are looked for (in the * specified order): * <ol> * <li>If there's a "documentBundles" property in the XWiki Preferences page then the XWiki documents listed there * (separated by commas) are considered the source for properties</li> * <li>If there's a "xwiki.documentBundles" property in the XWiki configuration file (xwiki.cfg) then the XWiki * documents listed there (separated by commas) are considered for source for properties</li> * <li>The Resource Bundle passed in the constructor</li> * </ol> * If the property is not found in any of these 3 sources then the key is returned in place of the value. In addition * the property values are cached for better performance but if one of the XWiki documents containing the properties is * modified, its content is cached again next time a key is asked. * * @version $Id: 214d1de32cc4756b93d6c13cfef6c790d35f9ffe $ * @deprecated since 4.3M2 use the {@link org.xwiki.localization.LocalizationManager} component instead */ @Deprecated public class XWikiMessageTool { /** * Log4J logger object to log messages in this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(XWikiMessageTool.class); /** * Property name used to defined internationalization document bundles in either XWikiProperties ("documentBundles") * or in the xwiki.cfg configuration file ("xwiki.documentBundles"). */ private static final String KEY = "documentBundles"; /** * Format string for the error message used to log load failures. */ private static final String LOAD_ERROR_MSG_FMT = "Failed to load internationalization document bundle [ %s ]."; /** * The default Resource Bundle to fall back to if no document bundle is found when trying to get a key. */ protected ResourceBundle bundle; /** * The {@link com.xpn.xwiki.XWikiContext} object, used to get access to XWiki primitives for loading documents. */ @Deprecated protected XWikiContext context; /** * Cache properties loaded from the document bundles for maximum efficiency. The map is of type (Long, Properties) * where Long is the XWiki document ids. */ private Map<Long, Properties> propsCache = new HashMap<Long, Properties>(); /** * Cache for saving the last modified dates of document bundles that have been loaded. This is used so that we can * reload them if they've been modified since last time they were cached. The map is of type (Long, Date) where Long * is the XWiki document ids. */ private Map<Long, Date> previousDates = new HashMap<Long, Date>(); /** * List of document bundles that have been modified since the last time they were cached. The Set contains Long * objects which are the XWiki document ids. TODO: This instance variable should be removed as it's used internally * and its state shouldn't encompass several calls to get(). */ private Set<Long> docsToRefresh = new HashSet<Long>(); /** * The localization manager. */ private ContextualLocalizationManager localization; /** * @param localization the localization manager */ public XWikiMessageTool(ContextualLocalizationManager localization) { this.localization = localization; } /** * @param bundle the default Resource Bundle to fall back to if no document bundle is found when trying to get a key * @param context the {@link com.xpn.xwiki.XWikiContext} object, used to get access to XWiki primitives for loading * documents */ public XWikiMessageTool(ResourceBundle bundle, XWikiContext context) { this.bundle = bundle; this.context = context; } protected XWikiContext getXWikiContext() { return this.context; } /** * @param key the key identifying the message to look for * @return the message in the defined language. The message should be a simple string without any parameters. If you * need to pass parameters see {@link #get(String, java.util.List)} * @see com.xpn.xwiki.web.XWikiMessageTool for more details on the algorithm used to find the message */ public String get(String key) { String translation; if (this.localization != null) { translation = get(key, ArrayUtils.EMPTY_OBJECT_ARRAY); } else { translation = getTranslation(key); if (translation == null) { try { translation = this.bundle.getString(key); } catch (Exception e) { translation = key; } } } return translation; } /** * Find a translation and then replace any parameters found in the translation by the passed params parameters. The * format is the one used by {@link java.text.MessageFormat}. * <p> * Note: The reason we're using a List instead of an Object array is because we haven't found how to easily create * an Array in Velocity whereas a List is easily created. For example: <code>$msg.get("key", ["1", "2", "3"])</code> * . * </p> * * @param key the key of the string to find * @param params the list of parameters to use for replacing "{N}" elements in the string. See * {@link java.text.MessageFormat} for the full syntax * @return the translated string with parameters resolved */ public String get(String key, List<?> params) { return get(key, params.toArray()); } /** * Find a translation and then replace any parameters found in the translation by the passed parameters. The format * is the one used by {@link java.text.MessageFormat}. * * @param key the key of the string to find * @param params the list of parameters to use for replacing "{N}" elements in the string. See * {@link java.text.MessageFormat} for the full syntax * @return the translated string with parameters resolved */ public String get(String key, Object... params) { String translation; if (this.localization != null) { translation = this.localization.getTranslationPlain(key, params); if (translation == null) { translation = key; } } else { translation = get(key); if (params != null && translation != null) { translation = MessageFormat.format(translation, params); } } return translation; } /** * @return the list of internationalization document bundle names as a list of XWiki page names ("Space.Document") * or an empty list if no such documents have been found * @see com.xpn.xwiki.web.XWikiMessageTool for more details on the algorithm used to find the document bundles */ protected List<String> getDocumentBundleNames() { List<String> docNamesList; XWikiContext context = getXWikiContext(); String docNames = context.getWiki().getXWikiPreference(KEY, context); if (docNames == null || "".equals(docNames)) { docNames = context.getWiki().Param("xwiki." + KEY); } if (docNames == null) { docNamesList = new ArrayList<String>(); } else { docNamesList = Arrays.asList(docNames.split(",")); } return docNamesList; } /** * @return the internationalization document bundles (a list of {@link XWikiDocument}) * @see com.xpn.xwiki.web.XWikiMessageTool for more details on the algorithm used to find the document bundles */ public List<XWikiDocument> getDocumentBundles() { XWikiContext context = getXWikiContext(); String defaultLanguage = context.getWiki().getDefaultLanguage(context); List<XWikiDocument> result = new ArrayList<XWikiDocument>(); for (String docName : getDocumentBundleNames()) { for (XWikiDocument docBundle : getDocumentBundles(docName.trim(), defaultLanguage)) { if (docBundle != null) { if (!docBundle.isNew()) { // Checks for a name update Long docId = Long.valueOf(docBundle.getId()); Date docDate = docBundle.getDate(); // Check for a doc modification if (!docDate.equals(this.previousDates.get(docId))) { this.docsToRefresh.add(docId); this.previousDates.put(docId, docDate); } result.add(docBundle); } else { // The document listed as a document bundle doesn't exist. Do nothing // and log. LOGGER.warn("The document [" + docBundle.getFullName() + "] is listed " + "as an internationalization document bundle but it does not " + "exist."); } } } } return result; } /** * Helper method to help get a translated version of a document. It handles any exception raised to make it easy to * use. * * @param documentName the document's name (eg Space.Document) * @return the document object corresponding to the passed document's name. A translated version of the document for * the current Locale is looked for. */ public XWikiDocument getDocumentBundle(String documentName) { XWikiDocument docBundle; if (documentName.length() == 0) { docBundle = null; } else { try { XWikiContext context = getXWikiContext(); // First, looks for a document suffixed by the language docBundle = context.getWiki().getDocument(documentName, context); docBundle = docBundle.getTranslatedDocument(context); } catch (XWikiException e) { // Error while loading the document. // TODO: A runtime exception should be thrown that will bubble up till the // topmost level. For now simply log the error LOGGER.error(String.format(LOAD_ERROR_MSG_FMT, documentName), e); docBundle = null; } } return docBundle; } /** * Helper method to help get a translated version of a document. It handles any exception raised to make it easy to * use. * * @param documentName the document's name (eg Space.Document) * @param defaultLanguage default language * @return the document object corresponding to the passed document's name. A translated version of the document for * the current Locale is looked for. */ public List<XWikiDocument> getDocumentBundles(String documentName, String defaultLanguage) { List<XWikiDocument> list = new ArrayList<XWikiDocument>(); if (documentName.length() != 0) { try { XWikiContext context = getXWikiContext(); // First, looks for a document suffixed by the language XWikiDocument docBundle = context.getWiki().getDocument(documentName, context); XWikiDocument tdocBundle = docBundle.getTranslatedDocument(context); list.add(tdocBundle); if (!tdocBundle.getRealLanguage().equals(defaultLanguage)) { XWikiDocument defdocBundle = docBundle.getTranslatedDocument(defaultLanguage, context); if (tdocBundle != defdocBundle) { list.add(defdocBundle); } } } catch (XWikiException e) { // Error while loading the document. // TODO: A runtime exception should be thrown that will bubble up till the // topmost level. For now simply log the error LOGGER.error(String.format(LOAD_ERROR_MSG_FMT, documentName), e); } } return list; } /** * @param docBundle the document bundle. * @return properties of the document bundle. */ public Properties getDocumentBundleProperties(XWikiDocument docBundle) { Properties props = new Properties(); String content = docBundle.getContent(); try { props.load(new StringReader(content)); } catch (IOException e) { LOGGER.error("Failed to parse content of document [" + docBundle + "] as translation content", e); } return props; } /** * Looks for a translation in the list of internationalization document bundles. It first checks if the translation * can be found in the cache. * * @param key the key identifying the translation * @return the translation or null if not found or if the passed key is null */ protected String getTranslation(String key) { String returnValue = null; if (key != null) { for (XWikiDocument docBundle : getDocumentBundles()) { if (docBundle != null) { Long docId = Long.valueOf(docBundle.getId()); Properties props = null; if (this.docsToRefresh.contains(docId) || !this.propsCache.containsKey(docId)) { // Cache needs to be updated props = getDocumentBundleProperties(docBundle); // updates cache this.propsCache.put(docId, props); this.docsToRefresh.remove(docId); } else { // gets from cache props = this.propsCache.get(docId); } returnValue = props.getProperty(key); if (returnValue != null) { break; } } } } return returnValue; } }