Code:
/**
* MessageSource implementation that reads the messages from a Datasource
*/
public class DataSourceMessageSource extends AbstractMessageSource implements InitializingBean {
/** the DataSource to use for reading in messages */
private DataSource dataSource;
/** used for filtering certain groups of messages (optional) */
private String[] basenames = new String[0];
/** how long the messages will be cached (before timestamps will be checked) <br/>
* a value below zero means "cache forever" */
private long cacheMillis = -1;
/** timestamp: when messages were loaded for the last time */
private long loadTimestamp = -1;
/** timestamp: when messages were last updated according to messageReader */
private long lastUpdate = -1;
/** Cache holding already generated MessageFormats per message code and Locale <br/>
* Map <String, Map <Locale, MessageFormat>> */
private final Map cachedMessageFormats = new HashMap();
/** all messages (for all basenames) per locale <br/>
* Map <Locale, properties> */
private Map cachedMergedProperties = new HashMap();
private String tableName = "resources";
private String codeColumn = "code";
private String languageColumn = "language";
private String countryColumn = "country";
private String variantColumn = "variant";
private String msgColumn = "msg";
private String basenameColumn = "basename";
private Locale fallbackLocale = Locale.ENGLISH;
private String baseSql;
public void init() {
refreshIfNecessary();
}
public void afterPropertiesSet() throws Exception {
logger.debug ("afterPropertiesSet");
computeBaseSql();
}
/**
* set the datasource
* @param dataSource the dataSource to use for loading messages
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* Set a single basename, must match exactly with value in tableName.basenameColumn
* @param basename the single basename
* @see #setBasenames
*/
public void setBasename(String basename) {
setBasenames(new String[]{basename});
}
/**
* Set an array of basenames
* @param basenames an array of basenames
* @see #setBasename
*/
public void setBasenames(String[] basenames) {
this.basenames = basenames;
}
/**
* set the name of the table to read messages from
* @param tableName name of table containing the messages
*/
public void setTableName(String tableName) {
this.tableName = tableName;
}
/**
* set the name of the column containing the message code
* @param codeColumn name of the column containing the message code
*/
public void setCodeColumn(String codeColumn) {
this.codeColumn = codeColumn;
}
/**
* set name of language column
* @param languageColumn name of column containing the language of the message
*/
public void setLanguageColumn(String languageColumn) {
this.languageColumn = languageColumn;
}
/** set name of the country column <br/>
* optional: use "null" if your table does not have a country column
* @param countryColumn name of country column
*/
public void setCountryColumn(String countryColumn) {
this.countryColumn = countryColumn;
}
/** set name of variant column <br/>
* optional: use "null" if your table does not have a variant column
* @param variantColumn
*/
public void setVariantColumn(String variantColumn) {
this.variantColumn = variantColumn;
}
/** set name of the message column
* @param msgColumn name of the column containing the localized message
*/
public void setMsgColumn(String msgColumn) {
this.msgColumn = msgColumn;
}
/** set name of basename column
* optional: use "null" if your table does not have a basename column
* @param basenameColumn
*/
public void setBasenameColumn(String basenameColumn) {
this.basenameColumn = basenameColumn;
}
/**
* set the Locale to fallback to when no match is found <br/><br/>
* set to null if you do not want a fallback Locale <br/>
* default is Locale.ENGLISH
* @param fallbackLocale the locale to fallback to when no match is found
*/
public void setFallbackLocale(Locale fallbackLocale) {
this.fallbackLocale = fallbackLocale;
}
/** set number of seconds to cache the messages
* <ul>
* <li>Default is "-1", indicating to cache forever
* <li>A positive number will cache loaded messages for the given number of seconds.
* This is essentially the interval between refresh attempts.
* Note that a refresh attempt will first check the last-modified timestamp using getLastUpdate
* <li>A value of "0" will check the last-modified timestamp on every message access.
* <b>Do not use this in a production environment!</b>
* </ul>
* @see #getLastUpdate
*/
public void setCacheSeconds (int cacheSeconds) {
this.cacheMillis = cacheSeconds * 1000;
}
/**
* build an array of alternative locales for the given locale <br/>
* result does not contain original locale
* @param locale the locale to find alternatives for
* @return an array of alternative locales
*/
private Locale[] getAlternativeLocales (Locale locale) {
Locale[] locales = new Locale[3];
int count = 0;
if (locale.getVariant().length() > 0) {
// add a locale without the variant
locales[count] = new Locale(locale.getLanguage(), locale.getCountry());
count++;
}
if (locale.getCountry().length() > 0) {
// add a locale without the country
locales[count] = new Locale(locale.getLanguage());
count++;
}
if (fallbackLocale != null) {
locales[count] = fallbackLocale;
}
return locales;
}
protected String internalResolveCodeWithoutArguments(String code, Locale locale) {
refreshIfNecessary();
String msg = getMessages(locale).getProperty(code);
if (msg != null)
return msg;
Locale[] locales = getAlternativeLocales(locale);
for (int i=0; i < locales.length; i++) {
msg = getMessages(locales[i]).getProperty(code);
if (msg != null)
return msg;
}
return null;
}
protected synchronized String resolveCodeWithoutArguments(String code, Locale locale) {
String msg = internalResolveCodeWithoutArguments(code, locale);
if (logger.isDebugEnabled())
logger.debug ("resolved [" + code + "] for locale [" + locale + "] => [" + msg +"]");
if (msg == null && logger.isInfoEnabled()) {
logger.info ("could not resolve [" + code + "] for locale [" + locale + "]");
}
return msg;
}
protected synchronized MessageFormat resolveCode(String code, Locale locale) {
refreshIfNecessary();
MessageFormat messageFormat = getMessageFormat (code, locale);
if (messageFormat != null)
return messageFormat;
Locale[] locales = getAlternativeLocales(locale);
for (int i=0; i < locales.length; i++) {
messageFormat = getMessageFormat(code, locales[i]);
if (messageFormat != null)
return messageFormat;
}
if (logger.isInfoEnabled()) {
logger.info ("could not resolve [" + code + "] for locale [" + locale + "]");
}
return null;
}
protected void refreshIfNecessary() {
if (loadTimestamp < 0) {
// loadMessages for the first time
readFromDataSource();
this.lastUpdate = getLastUpdate();
return;
}
if (cacheMillis < 0) {
return;
}
if (System.currentTimeMillis() > loadTimestamp + cacheMillis) {
// time to check if messages have been updated
long lastUpdate = getLastUpdate();
if (lastUpdate != this.lastUpdate) {
// messages have changed => read them in again
this.lastUpdate = lastUpdate;
readFromDataSource();
}
}
}
/**
* create a locale from given values, supporting null-values for country and variant
* @param language language to construct Locale for
* @param country country to construct Locale for
* @param variant variant to construct Locale for
* @return a Locale object
* @throws NullPointerException if language is null
*/
private Locale createLocale (String language, String country, String variant) {
if (country == null)
return new Locale(language);
if (variant == null)
return new Locale(language,country);
return new Locale(language,country,variant);
}
/**
* get the messages for the given locale, creating a new Properties object if necessary
* @param locale the locale to find messages for
* @return a Properties object
*/
private Properties getMessages (Locale locale) {
Properties messages = (Properties) cachedMergedProperties.get(locale);
if (messages == null) {
messages = new Properties();
cachedMergedProperties.put(locale, messages);
}
return messages;
}
/**
* stores a message in our internal data structures
* @param code the code of the message to store
* @param language the language of the message (required)
* @param country the country of the message (optional, may be null)
* @param variant the variant of the message (optional, may be null)
* @param message the actual message
*/
public void mapMessage (String code, String language, String country, String variant, String message) {
Locale locale = createLocale(language, country, variant);
Properties messages = getMessages (locale);
if (logger.isDebugEnabled())
logger.debug ("adding message [" + message + "] for code [" + code + "] and locale [" + locale +"]");
messages.setProperty(code, message);
}
protected MessageFormat getMessageFormat(String code, Locale locale) {
Map localeMap = (Map) this.cachedMessageFormats.get(code);
if (localeMap != null) {
MessageFormat result = (MessageFormat) localeMap.get(locale);
if (result != null) {
return result;
}
}
String msg = getMessages(locale).getProperty(code);
if (msg != null) {
if (localeMap == null) {
localeMap = new HashMap();
this.cachedMessageFormats.put(code, localeMap);
}
MessageFormat result = createMessageFormat(msg, locale);
localeMap.put(locale, result);
return result;
}
return null;
}
private void computeBaseSql() {
StringBuffer sql = new StringBuffer("SELECT ");
sql.append(codeColumn).append(" as code, ");
sql.append(languageColumn).append(" as lang, ");
sql.append(countryColumn).append(" as country, ");
sql.append(variantColumn).append(" as variant, ");
sql.append(msgColumn).append(" as msg ");
sql.append("FROM ").append(tableName);
baseSql = sql.toString();
}
/**
* This method should return the timestamp when messages were last updated. <br/>
* The default implementation always returns -1, which will prevent refreshing. <br/>
* sub-classes should override this method when refreshing is desired
* @return -1
*/
public long getLastUpdate() {
return -1; // never refresh
}
private final void readFromDataSource() {
readMessages();
loadTimestamp = System.currentTimeMillis();
}
protected void readMessages() {
cachedMergedProperties.clear();
cachedMessageFormats.clear();
long startTime = System.currentTimeMillis();
if (baseSql == null)
computeBaseSql();
StringBuffer sql = new StringBuffer(baseSql);
if (basenames.length > 0) {
sql.append(" WHERE ").append(basenameColumn).append(" = ?");
}
for (int i=1; i < basenames.length; i++) {
sql.append(" OR ").append(basenameColumn).append(" = ?");
}
if (logger.isDebugEnabled()) {
logger.debug("sql=[" + sql + "]");
}
MappingSqlQuery query = new MappingSqlQuery(dataSource, sql.toString()) {
protected Object mapRow(ResultSet rs, int rowNum) throws SQLException {
String code = rs.getString("code");
String language = rs.getString("lang");
String country = rs.getString("country");
String variant = rs.getString("variant");
String msg = rs.getString("msg");
mapMessage(code, language, country, variant, msg);
return null;
}
};
query.setTypes(createIntArray(basenames.length,Types.VARCHAR));
query.compile();
query.execute(basenames);
long millis = System.currentTimeMillis() - startTime;
logger.info ("readMessages took " + millis + " millis");
}
/**
* create an int array of given length with all elements initialized
* with given value
* @param length length of array to create
* @param value all elements will be set to this value
* @return an int-array of given length
*/
private static int[] createIntArray (int length, int value) {
int result[] = new int[length];
for (int i=0; i<length; i++) {
result[i] = value;
}
return result;
}
}