Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to put JSF message bundle outside of WAR so it can be edited without redeployment?

We have a JSF application on WildFly 8 which uses the traditionally mechanism with internationalizing text by having message bundles for German and English in the WEB-INF\classes folder of the WAR and a configuration in faces-config.xml mapping a name to it and listing the locales. The application does not have a database connection, but uses REST services to communicate with a 2nd application.

Now we need to be able to change text more easily, meaning not having to build a new WAR file and do a deployment when changing a text. So I need a mechanism to have the message bundles outside of the WAR while being able to use it as before within the XHTML pages.

Two optional requirements would be to change the text and refresh the messages in the application without having to restart the application (priority 2), and to have a default bundle within the WAR, which is overwritten by the external bundle (priority 3).

My thought was to use something like Apache commons configuration to read a property file within an Application scoped bean and expose a getter under the EL name used before. But somehow it feels like having to re-implement an existing mechanism and that this should somehow be easier, maybe even with Java EE core only.

Has someone used this mechanism in such a way and can point me to some example/description on the details or has a better idea to implement the listed requirement(s)?

like image 992
Alexander Rühl Avatar asked Jan 29 '16 13:01

Alexander Rühl


1 Answers

How to put JSF message bundle outside of WAR?

Two ways:

  1. Add its path to the runtime classpath of the server.

  2. Create a custom ResourceBundle implementation with a Control.


change the text and refresh the messages in the application without having to restart the application

Changing the text will be trivial. However, refreshing is not trivial. Mojarra internally caches it agressively. This has to be taken into account in case you want to go for way 1. Arjan Tijms has posted a Mojarra specific trick to clear its internal resource bundle cache in this related question: How to reload resource bundle in web application?

If changing the text happens in the webapp itself, then you could simply perform the cache cleanup in the save method. If changing the text however can happen externally, then you'd need to register a file system watch service to listen on changes (tutorial here) and then either for way 1 clear the bundle cache, or for way 2 reload internally in handleGetObject().


have a default bundle within the WAR, which is overwritten by the external bundle

When loading them from classpath, the default behavior is the other way round (resources in WAR have higher classloading precedence), so this definitely scratches way 1 and leaves us with way 2.

Below is a kickoff example of way 2. This assumes that you're using property resource bundles with a base name of text (i.e. no package) and that the external path is located in /var/webapp/i18n.

public class YourBundle extends ResourceBundle {

    protected static final Path EXTERNAL_PATH = Paths.get("/var/webapp/i18n");
    protected static final String BASE_NAME = "text";
    protected static final Control CONTROL = new YourControl();

    private static final WatchKey watcher;

    static {
        try {
            watcher = EXTERNAL_PATH.register(FileSystems.getDefault().newWatchService(), StandardWatchEventKinds.ENTRY_MODIFY);
        } catch (IOException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private Path externalResource;
    private Properties properties;

    public YourBundle() {
        Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
        setParent(ResourceBundle.getBundle(BASE_NAME, locale, CONTROL));
    }

    private YourBundle(Path externalResource, Properties properties) {
        this.externalResource = externalResource;
        this.properties = properties;
    }

    @Override
    protected Object handleGetObject(String key) {
        if (properties != null) {
            if (!watcher.pollEvents().isEmpty()) { // TODO: this is naive, you'd better check resource name if you've multiple files in the folder and keep track of others.
                synchronized(properties) {
                    try (InputStream input = new FileInputStream(externalResource.toFile())) {
                        properties.load(input);
                    } catch (IOException e) {
                        throw new IllegalStateException(e);
                    }
                }
            }

            return properties.get(key);
        }

        return parent.getObject(key);
    }

    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public Enumeration<String> getKeys() {
        if (properties != null) {
            Set keys = properties.keySet();
            return Collections.enumeration(keys);
        }

        return parent.getKeys();
    }

    protected static class YourControl extends Control {

        @Override
        public ResourceBundle newBundle
            (String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
                throws IllegalAccessException, InstantiationException, IOException
        {
            String resourceName = toResourceName(toBundleName(baseName, locale), "properties");
            Path externalResource = EXTERNAL_PATH.resolve(resourceName);
            Properties properties = new Properties();

            try (InputStream input = loader.getResourceAsStream(resourceName)) {
                properties.load(input); // Default (internal) bundle.
            }

            try (InputStream input = new FileInputStream(externalResource.toFile())) {
                properties.load(input); // External bundle (will overwrite same keys).
            }

            return new YourBundle(externalResource, properties);
        }

    }

}

In order to get it to run, register as below in faces-config.xml.

<application>
    <resource-bundle>
        <base-name>com.example.YourBundle</base-name>
        <var>i18n</var>
    </resource-bundle>
</application>
like image 69
BalusC Avatar answered Nov 04 '22 09:11

BalusC