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

旧城冷巷雨未停 提交于 2019-12-01 05:59:27
BalusC

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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!