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

1

6

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)?

Adjournment answered 29/1, 2016 at 13:47 Comment(4)
Is this helpful? https://mcmap.net/q/974706/-internationalization-in-jsf-with-resourcebundle-entries-which-are-loaded-from-databaseMinority
@Minority Well, haven't looked into details of the question when stumbling upon before, as it refers to handling it via database, which I don't have here - but I guess you are referring to the part of extending the ResourceBundle? So in the getItSomehowpart it must then be loaded via file operation? In that case it could be a way to handle it. Only the two optional requirements are not clear to be covered here.Sensual
@Minority Ok, 2) makes sense, 1) is maybe misunderstood - I don't need to reflect changes back to the file, but be able to change the file and then trigger a reload of the bundle. - If you like to spend the time to put the comments into an answer, I'm happy to assign the bounty.Sensual
There is no way to achieve the wanted behaviour with the provided ResourceBundle.getBundle(...) methods. The provided implementation uses an internal lookup map (cacheList) and thus each ResourceBundle is loaded only once. For my purpose I wrote a replacement for the Internationalization Tag Library (fmt) to use files and reload those if changed.Castaway
M
9

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>
Minority answered 10/2, 2016 at 19:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.