Announcement Announcement Module
Collapse
No announcement yet.
AbstractMessageSource using DB table instead of props file? Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • AbstractMessageSource using DB table instead of props file?

    Hi,
    Has anyone written an implementation of AbstractMessageSource which uses a database table as its message source, instead of a properties file? I did this for Struts' PropertyMessageResources, and now have to do it for Spring's AbstractMessageSource.

    Before I did so, I thought I'd check to see if there's one already publicly available...

    Thanks,
    Eric Jung

  • #2
    I did.
    I was thinking to give end-users the ability to add/change messages live but I'm not using it any longer for several reasons.
    1) unconvenient during development: for every new message you need to write an insert statement (or create a management app)
    2) keeping your messages in CVS and in the production database causes headaches

    I am now using an MessageSource that reads from an xml file.

    If there's interest I could post this class in Jira (and the one reading from xml).

    Maybe synchronization can be improved.

    Maarten
    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 &#40;before timestamps will be checked&#41; <br/>
       * a value below zero means "cache forever"  */
      private long cacheMillis = -1;
    
      /** timestamp&#58; when messages were loaded for the last time  */
      private long loadTimestamp = -1;
    
      /** timestamp&#58; 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&gt;&gt;  */
      private final Map cachedMessageFormats = new HashMap&#40;&#41;;
    
      /** all messages &#40;for all basenames&#41; per locale <br/>
       * Map <Locale, properties&gt;  */
      private Map cachedMergedProperties = new HashMap&#40;&#41;;
    
      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&#40;&#41; &#123;
        refreshIfNecessary&#40;&#41;;
      &#125;
    
      public void afterPropertiesSet&#40;&#41; throws Exception &#123;
        logger.debug &#40;"afterPropertiesSet"&#41;;
        computeBaseSql&#40;&#41;;
      &#125;
    
      /**
       * set the datasource
       * @param dataSource the dataSource to use for loading messages
       */
      public void setDataSource&#40;DataSource dataSource&#41; &#123;
        this.dataSource = dataSource;
      &#125;
    
      /**
       * Set a single basename, must match exactly with value in tableName.basenameColumn
       * @param basename the single basename
       * @see #setBasenames
       */
      public void setBasename&#40;String basename&#41; &#123;
        setBasenames&#40;new String&#91;&#93;&#123;basename&#125;&#41;;
      &#125;
    
      /**
       * Set an array of basenames
       * @param basenames an array of basenames
       * @see #setBasename
       */
      public void setBasenames&#40;String&#91;&#93; basenames&#41; &#123;
        this.basenames = basenames;
      &#125;
    
      /**
       * set the name of the table to read messages from
       * @param tableName name of table containing the messages
       */
      public void setTableName&#40;String tableName&#41; &#123;
        this.tableName = tableName;
      &#125;
    
      /**
       * set the name of the column containing the message code
       * @param codeColumn name of the column containing the message code
       */
      public void setCodeColumn&#40;String codeColumn&#41; &#123;
        this.codeColumn = codeColumn;
      &#125;
    
      /**
       * set name of language column
       * @param languageColumn name of column containing the language of the message
       */
      public void setLanguageColumn&#40;String languageColumn&#41; &#123;
        this.languageColumn = languageColumn;
      &#125;
    
      /** set name of the country column <br/>
       * optional&#58; use "null" if your table does not have a country column
       * @param countryColumn name of country column
       */
      public void setCountryColumn&#40;String countryColumn&#41; &#123;
        this.countryColumn = countryColumn;
      &#125;
    
      /** set name of variant column <br/>
       * optional&#58; use "null" if your table does not have a variant column
       * @param variantColumn
       */
      public void setVariantColumn&#40;String variantColumn&#41; &#123;
        this.variantColumn = variantColumn;
      &#125;
    
      /** set name of the message column
       * @param msgColumn name of the column containing the localized message
       */
      public void setMsgColumn&#40;String msgColumn&#41; &#123;
        this.msgColumn = msgColumn;
      &#125;
    
      /** set name of basename column
       * optional&#58; use "null" if your table does not have a basename column
       * @param basenameColumn
       */
      public void setBasenameColumn&#40;String basenameColumn&#41; &#123;
        this.basenameColumn = basenameColumn;
      &#125;
    
      /**
       * 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&#40;Locale fallbackLocale&#41; &#123;
        this.fallbackLocale = fallbackLocale;
      &#125;
    
      /** 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 &#40;int cacheSeconds&#41; &#123;
        this.cacheMillis = cacheSeconds * 1000;
      &#125;
    
      /**
       * 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&#91;&#93; getAlternativeLocales &#40;Locale locale&#41; &#123;
        Locale&#91;&#93; locales = new Locale&#91;3&#93;;
        int count = 0;
        if &#40;locale.getVariant&#40;&#41;.length&#40;&#41; > 0&#41; &#123;
          // add a locale without the variant
          locales&#91;count&#93; =  new Locale&#40;locale.getLanguage&#40;&#41;, locale.getCountry&#40;&#41;&#41;;
          count++;
        &#125;
        if &#40;locale.getCountry&#40;&#41;.length&#40;&#41; > 0&#41; &#123;
          // add a locale without the country
          locales&#91;count&#93; =  new Locale&#40;locale.getLanguage&#40;&#41;&#41;;
          count++;
        &#125;
        if &#40;fallbackLocale != null&#41; &#123;
          locales&#91;count&#93; = fallbackLocale;
        &#125;
        return locales;
      &#125;
    
      protected String internalResolveCodeWithoutArguments&#40;String code, Locale locale&#41; &#123;
        refreshIfNecessary&#40;&#41;;
        String msg = getMessages&#40;locale&#41;.getProperty&#40;code&#41;;
        if &#40;msg != null&#41;
          return msg;
        Locale&#91;&#93; locales = getAlternativeLocales&#40;locale&#41;;
        for &#40;int i=0; i < locales.length; i++&#41; &#123;
          msg = getMessages&#40;locales&#91;i&#93;&#41;.getProperty&#40;code&#41;;
          if &#40;msg != null&#41;
            return msg;
        &#125;
        return null;
      &#125;
    
      protected synchronized String resolveCodeWithoutArguments&#40;String code, Locale locale&#41; &#123;
        String msg = internalResolveCodeWithoutArguments&#40;code, locale&#41;;
        if &#40;logger.isDebugEnabled&#40;&#41;&#41;
          logger.debug &#40;"resolved &#91;" + code + "&#93; for locale &#91;" + locale + "&#93; => &#91;" + msg +"&#93;"&#41;;
        if &#40;msg == null && logger.isInfoEnabled&#40;&#41;&#41; &#123;
          logger.info &#40;"could not resolve &#91;" + code + "&#93; for locale &#91;" + locale + "&#93;"&#41;;
        &#125;
        return msg;
      &#125;
    
      protected synchronized MessageFormat resolveCode&#40;String code, Locale locale&#41; &#123;
        refreshIfNecessary&#40;&#41;;
        MessageFormat messageFormat = getMessageFormat &#40;code, locale&#41;;
        if &#40;messageFormat != null&#41;
          return messageFormat;
        Locale&#91;&#93; locales = getAlternativeLocales&#40;locale&#41;;
        for &#40;int i=0; i < locales.length; i++&#41; &#123;
          messageFormat = getMessageFormat&#40;code, locales&#91;i&#93;&#41;;
          if &#40;messageFormat != null&#41;
            return messageFormat;
        &#125;
        if &#40;logger.isInfoEnabled&#40;&#41;&#41; &#123;
          logger.info &#40;"could not resolve &#91;" + code + "&#93; for locale &#91;" + locale + "&#93;"&#41;;
        &#125;
        return null;
      &#125;
    
      protected void refreshIfNecessary&#40;&#41; &#123;
        if &#40;loadTimestamp < 0&#41; &#123;
          // loadMessages for the first time
          readFromDataSource&#40;&#41;;
          this.lastUpdate = getLastUpdate&#40;&#41;;
          return;
        &#125;
        if &#40;cacheMillis < 0&#41; &#123;
          return;
        &#125;
        if &#40;System.currentTimeMillis&#40;&#41; > loadTimestamp + cacheMillis&#41; &#123;
          // time to check if messages have been updated
          long lastUpdate = getLastUpdate&#40;&#41;;
          if &#40;lastUpdate != this.lastUpdate&#41; &#123;
            // messages have changed => read them in again
            this.lastUpdate = lastUpdate;
            readFromDataSource&#40;&#41;;
          &#125;
        &#125;
      &#125;
    
      /**
       * 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 &#40;String language, String country, String variant&#41; &#123;
        if &#40;country == null&#41;
          return new Locale&#40;language&#41;;
        if &#40;variant == null&#41;
          return new Locale&#40;language,country&#41;;
        return new Locale&#40;language,country,variant&#41;;
      &#125;
    
      /**
       * 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 &#40;Locale locale&#41; &#123;
        Properties messages = &#40;Properties&#41; cachedMergedProperties.get&#40;locale&#41;;
        if &#40;messages == null&#41; &#123;
          messages = new Properties&#40;&#41;;
          cachedMergedProperties.put&#40;locale, messages&#41;;
        &#125;
        return messages;
      &#125;
    
      /**
       * stores a message in our internal data structures
       * @param code the code of the message to store
       * @param language the language of the message &#40;required&#41;
       * @param country the country of the message &#40;optional, may be null&#41;
       * @param variant the variant of the message &#40;optional, may be null&#41;
       * @param message the actual message
       */
      public void mapMessage &#40;String code, String language, String country, String variant, String message&#41; &#123;
        Locale locale = createLocale&#40;language, country, variant&#41;;
        Properties messages = getMessages &#40;locale&#41;;
        if &#40;logger.isDebugEnabled&#40;&#41;&#41;
          logger.debug &#40;"adding message &#91;" + message + "&#93; for code &#91;" + code + "&#93; and locale &#91;" + locale +"&#93;"&#41;;
        messages.setProperty&#40;code, message&#41;;
      &#125;
    
      protected MessageFormat getMessageFormat&#40;String code, Locale locale&#41; &#123;
        Map localeMap = &#40;Map&#41; this.cachedMessageFormats.get&#40;code&#41;;
        if &#40;localeMap != null&#41; &#123;
          MessageFormat result = &#40;MessageFormat&#41; localeMap.get&#40;locale&#41;;
          if &#40;result != null&#41; &#123;
            return result;
          &#125;
        &#125;
        String msg = getMessages&#40;locale&#41;.getProperty&#40;code&#41;;
        if &#40;msg != null&#41; &#123;
          if &#40;localeMap == null&#41; &#123;
            localeMap = new HashMap&#40;&#41;;
            this.cachedMessageFormats.put&#40;code, localeMap&#41;;
          &#125;
          MessageFormat result = createMessageFormat&#40;msg, locale&#41;;
          localeMap.put&#40;locale, result&#41;;
          return result;
        &#125;
        return null;
      &#125;
    
      private void computeBaseSql&#40;&#41; &#123;
        StringBuffer sql = new StringBuffer&#40;"SELECT "&#41;;
        sql.append&#40;codeColumn&#41;.append&#40;" as code, "&#41;;
        sql.append&#40;languageColumn&#41;.append&#40;" as lang, "&#41;;
        sql.append&#40;countryColumn&#41;.append&#40;" as country, "&#41;;
        sql.append&#40;variantColumn&#41;.append&#40;" as variant, "&#41;;
        sql.append&#40;msgColumn&#41;.append&#40;" as msg "&#41;;
        sql.append&#40;"FROM "&#41;.append&#40;tableName&#41;;
        baseSql = sql.toString&#40;&#41;;
      &#125;
    
      /**
       * 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&#40;&#41; &#123;
        return -1; // never refresh
      &#125;
    
      private final void readFromDataSource&#40;&#41; &#123;
        readMessages&#40;&#41;;
        loadTimestamp = System.currentTimeMillis&#40;&#41;;
      &#125;
    
      protected void readMessages&#40;&#41; &#123;
        cachedMergedProperties.clear&#40;&#41;;
        cachedMessageFormats.clear&#40;&#41;;
        long startTime = System.currentTimeMillis&#40;&#41;;
        if &#40;baseSql == null&#41;
          computeBaseSql&#40;&#41;;
        StringBuffer sql = new StringBuffer&#40;baseSql&#41;;
        if &#40;basenames.length > 0&#41; &#123;
          sql.append&#40;" WHERE "&#41;.append&#40;basenameColumn&#41;.append&#40;" = ?"&#41;;
        &#125;
        for &#40;int i=1; i < basenames.length; i++&#41; &#123;
          sql.append&#40;" OR "&#41;.append&#40;basenameColumn&#41;.append&#40;" = ?"&#41;;
        &#125;
        if &#40;logger.isDebugEnabled&#40;&#41;&#41; &#123;
          logger.debug&#40;"sql=&#91;" + sql + "&#93;"&#41;;
        &#125;
        MappingSqlQuery query = new MappingSqlQuery&#40;dataSource, sql.toString&#40;&#41;&#41; &#123;
          protected Object mapRow&#40;ResultSet rs, int rowNum&#41; throws SQLException &#123;
            String code = rs.getString&#40;"code"&#41;;
            String language = rs.getString&#40;"lang"&#41;;
            String country = rs.getString&#40;"country"&#41;;
            String variant = rs.getString&#40;"variant"&#41;;
            String msg = rs.getString&#40;"msg"&#41;;
            mapMessage&#40;code, language, country, variant, msg&#41;;
            return null;
          &#125;
        &#125;;
        query.setTypes&#40;createIntArray&#40;basenames.length,Types.VARCHAR&#41;&#41;;
        query.compile&#40;&#41;;
        query.execute&#40;basenames&#41;;
        long millis = System.currentTimeMillis&#40;&#41; - startTime;
        logger.info &#40;"readMessages took " + millis + " millis"&#41;;
      &#125;
    
      /**
       * 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&#91;&#93; createIntArray &#40;int length, int value&#41; &#123;
        int result&#91;&#93; = new int&#91;length&#93;;
        for &#40;int i=0; i<length; i++&#41; &#123;
          result&#91;i&#93; = value;
        &#125;
        return result;
      &#125;
    
    &#125;

    Comment


    • #3
      Thanks. I actually implemented one yesterday when I didn't get any replies to this thread. I'll compare mine with yours--I'm curious to see how we both solved the same problem.

      Comment


      • #4
        Good thread!

        However I seam to have problem, getting error message:

        ERROR - No message found under code 'adminUser.firstname' for locale 'sv'.
        javax.servlet.jsp.JspTagException: No message found under code 'adminUser.firstname' for locale 'sv'.
        at org.springframework.web.servlet.tags.MessageTag.do StartTagInternal(MessageTag.java:183)


        Have defined the bean in my xml lite this:
        <bean id="messageSource" class="com.mycompany.DataSourceMessageSource">
        <property name="dataSource"><ref bean="dataSource"/></property>
        <property name="tableName"><value>resources</value></property>
        <property name="codeColumn"><value>code</value></property>
        <property name="languageColumn"><value>language</value></property>
        <property name="countryColumn"><value>country</value></property>
        <property name="variantColumn"><value>variant</value></property>
        <property name="msgColumn"><value>msg</value></property>
        <property name="basenameColumn"><value>basename</value></property>
        <property name="cacheSeconds"><value>10</value></property>
        </bean>

        And are trying to get the text in my jsp with
        <spring:message code="adminUser.firstname"/>

        it sure looks like the map is populated with the textmessage and the key adminUser.firstname ....

        In my database I have 'sv' for language and 'SE' for country and 'adminUser.firstname' for code column

        any clues?

        Comment


        • #5
          Solved it,
          Using M$ explorer could cause unwanted results...

          Comment

          Working...
          X