I needed to create my own text provider. The idea here is that the text provider reads out of a database table. This means I can have a "meaning" table that I can allow the user to modify and change the text of their application without modifying any files.
The first step is to create a custom ResourceBundle that will replace the one that comes with Struts 2.
Note in the examples that I'm using Spring for IOC injection.
Here is the ResourceBundle, there are some extra methods for the user interface piece, but it basically loads the meaning table at runtime, and will provide text for the app:
public class DatabaseResourceBundle extends ResourceBundle implements ServletContextAware { private BasicDataSource ds; private Hashtable<String, List<String>> ht; private ServletContext ctx; private boolean needsLoaded = true; private static Logger l = Logger.getLogger(DatabaseResourceBundle.class); public DatabaseResourceBundle() { super(); } public void setServletContext(ServletContext c) { l.debug("ServletContext injected"); this.ctx = c; } public ServletContext getServletContext() { return this.ctx; } private void loadData() { l.debug("Loading resources"); ResultSetHandler h = new ResultSetHandler() { // Sets up a hashtable of <Key, List> // where the list is explanation, message public Object handle(ResultSet rs) throws SQLException { Hashtable<String, List<String>> h = new Hashtable<String, List<String>>(); while(rs.next()) { List<String> expMsg = new ArrayList<String>(3); expMsg.add(rs.getString(2)); expMsg.add(rs.getString(3)); expMsg.add(rs.getString(4)); h.put(rs.getString(1), expMsg); } return h; } }; QueryRunner run = new QueryRunner(this.getDataSource()); try { this.ht = (Hashtable<String, List<String>>) run.query("SELECT string_key, string_explanation, string_message, application_name FROM smc_meaning", h); return; } catch(Exception e) { l.error("Trying to initilize SMC Appsuite database: " + e.toString()); } } public void setConfigDataSource(BasicDataSource d) throws Exception { this.ds = d; } public void updateMeaning(String key, String newMeaning) throws Exception { l.debug("Saving " + key + " as " + newMeaning); List<String> message = this.ht.get(key); message.add(1, newMeaning); this.ht.put(key, message); l.debug("Updated text provider hash"); String stmt = "UPDATE smc_meaning set string_message = ? WHERE string_key = ?"; Object[] params = new Object[]{newMeaning, key}; QueryRunner run = new QueryRunner(this.getDataSource()); run.update(stmt, params); l.debug("Updated database"); // Force a refresh for the bean. //this.setConfigDataSource(this.getDataSource()); } public BasicDataSource getDataSource() { return this.ds; } public void verifyLoaded() { if(this.needsLoaded) { // Load data will inititlize things, if need-be this.loadData(); this.needsLoaded = false; } else { } } public Enumeration<String> getKeys() { verifyLoaded(); return this.ht.keys(); } public String getExplanation(String key) { verifyLoaded(); if(this.ht.get(key) != null) { return ((List<String>)this.ht.get(key)).get(0); } else { return key; } } public String get(String key) { verifyLoaded(); //l.debug("Getting value for: " + key); if(this.ht.get(key) != null) { return ((List<String>)this.ht.get(key)).get(1); } else { return key; } } public Hashtable<String, List<String>>getAllMeanings() { verifyLoaded(); return this.ht; } protected Object handleGetObject(String key) { verifyLoaded(); return (Object) this.get(key); } }
Next, I create a base action that all my struts actions extend, and it uses this DatabaseResourceBundle (injected via Spring, configured in WEB-INF/applicationContext.xml):
public class BaseAction extends ActionSupport implements SessionAware, Preparable { private ResourceBundle bundle; /* Irrelevant code snipped */ public String getText(String key) { try { return ((DatabaseResourceBundle)this.bundle).get(key); } catch(Exception e) { return key; } } public String getText(String key, List<Object> args) { return(getText(key)); } public String getText(String key, String defaultValue) { try { return ((DatabaseResourceBundle)this.bundle).get(key); } catch(Exception e) { return(defaultValue); } } public String getText(String key, String[] args) { return(getText(key)); } public String getText(String key, String defaultValue, List<Object> args) { return(getText(key)); } public String getText(String key, String defaultValue, List<Object> args, ValueStack stack) { return(getText(key)); } public String getText(String key, String defaultValue, String obj) { return(getText(key)); } public String getText(String key, String defaultValue, String[] args) { return(getText(key)); } public String getText(String key, String defaultValue, String[] args, ValueStack stack) { return(getText(key)); } public ResourceBundle getTexts() { return this.bundle; } public ResourceBundle getTexts(String bundleName) { return this.bundle; } public boolean hasKey(String key) { return this.bundle.keySet().contains(key); } }
Create the following database table in your DB (I use derby, but whatever will work):
create table smc_meaning ( id integer not null GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1), application_name varchar(50) NOT NULL, string_key varchar(50) NOT NULL, string_explanation varchar(2048), string_message varchar(2048) );
Populate the table, and off you go.