package org.xdi.oxauth.i18n;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;

import javax.faces.context.FacesContext;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;

/**
 * Custom i18n resource loader
 *
 * @author Yuriy Movchan
 * @version 02/23/2018
 */
public class CustomResourceBundle extends ResourceBundle {

    private static final Logger LOG = Logger.getLogger(CustomResourceBundle.class);

    private static final String BASE_NAME = "messages";
    private static final Control CONTROL = new CustomControl();

    private static Path EXTERNAL_PATH;
    private static WatchKey WATCHER = null;
    private static Date WATCHER_LAST_UPDATE = new Date();

    static {
        if (System.getProperty("server.base") != null) {
            Path customPath = Paths.get(System.getProperty("server.base") + "/custom/i18n");
            File file = customPath.toFile();
            if (file.exists() && file.isDirectory()) {
                EXTERNAL_PATH = customPath;
            }
        }
        try {
            if (EXTERNAL_PATH != null) {
                WATCHER = EXTERNAL_PATH.register(FileSystems.getDefault().newWatchService(), StandardWatchEventKinds.ENTRY_MODIFY);
            }
        } catch (IOException ex) {
            LOG.error("Failed to initialize custom i18n messages watcher service");
        }
    }

    private final ReentrantLock updateLock = new ReentrantLock();
    private Path externalResource;
    private Properties properties;
    private Date lastUpdate;

    public CustomResourceBundle() {
        Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
        if ((EXTERNAL_PATH != null) && (WATCHER != null)) {
            setParent(ResourceBundle.getBundle(BASE_NAME, locale, CONTROL));
        } else {
            setParent(ResourceBundle.getBundle(BASE_NAME, locale));
        }
        
        this.lastUpdate = new Date();
    }

    private CustomResourceBundle(Path externalResource, Properties properties) {
        this.externalResource = externalResource;
        this.properties = properties;
        
        this.lastUpdate = new Date();
    }

    @Override
    protected Object handleGetObject(String key) {
        if (properties != null) {
            checkWatcher();
            
            updateLock.lock();
            try {
                if (WATCHER_LAST_UPDATE.after(this.lastUpdate)) {
                    InputStream input = null;
                    try {
                        input = new FileInputStream(externalResource.toFile());
                        properties.load(input);
                    } catch (IOException ex) {
                        throw new IllegalStateException(ex);
                    } finally {
                        if (input != null) {
                            IOUtils.closeQuietly(input);
                        }
                    }
                    this.lastUpdate = new Date();
                }
            } finally {
                updateLock.unlock();
            }

            return properties.get(key);
        }

        return parent.getObject(key);
    }

    private void checkWatcher() {
        if (!WATCHER.pollEvents().isEmpty()) {
            WATCHER_LAST_UPDATE = new Date();
        }
    }

    @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 CustomControl 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();

            InputStream input = null;
            try {
                input = loader.getResourceAsStream(resourceName);
                properties.load(input); // Default (internal) bundle.
            } finally {
                if (input != null) {
                    IOUtils.closeQuietly(input);
                }
            }

            input = null;
            try {
                File file = externalResource.toFile();
                if (file.exists()) {
                    input = new FileInputStream(externalResource.toFile());
                    properties.load(input); // External bundle (will overwrite same keys).
                }
            } finally {
                if (input != null) {
                    IOUtils.closeQuietly(input);
                }
            }

            return new CustomResourceBundle(externalResource, properties);
        }

    }

}

@@ -34,678 +34,49 @@
 ort 
-java.io.File;%0Aimport java.io.FileInputStream;%0Aimport java.io.IOException;%0Aimport java.io.InputStream;%0Aimport java.nio.file.FileSystems;%0Aimport java.nio.file.Path;%0Aimport java.nio.file.Paths;%0Aimport java.nio.file.StandardWatchEventKinds;%0Aimport java.nio.file.WatchKey;%0Aimport java.util.Collections;%0Aimport java.util.Date;%0Aimport java.util.Enumeration;%0Aimport java.util.Locale;%0Aimport java.util.Properties;%0Aimport java.util.ResourceBundle;%0Aimport java.util.Set;%0Aimport java.util.concurrent.atomic.AtomicBoolean;%0Aimport java.util.concurrent.locks.ReentrantLock;%0A%0Aimport javax.faces.context.FacesContext;%0A%0Aimport org.apache.commons.io.IOUtils;%0Aimport org.apache.log4j.Logger
+org.gluu.jsf2.i18n.ExtendedResourceBundle
 ;%0A%0A/
@@ -210,87 +210,16 @@
 nds 
-ResourceBundle %7B%0A%0A    private static final Logger LOG = Logger.getLogger(Custom
+Extended
 Reso
@@ -232,16 +232,10 @@
 ndle
-.class);
+ %7B
 %0A%0A  
@@ -296,4485 +296,82 @@
     
-private static final Control CONTROL = new CustomControl();%0A%0A    private static Path EXTERNAL_PATH;%0A    private static WatchKey WATCHER = null;%0A    private static Date WATCHER_LAST_UPDATE = new Date();%0A%0A    static %7B%0A        if (System.getProperty(%22server.base%22) != null) %7B%0A            Path customPath = Paths.get(System.getProperty(%22server.base%22) + %22/custom/i18n%22);%0A            File file = customPath.toFile();%0A            if (file.exists() && file.isDirectory()) %7B%0A                EXTERNAL_PATH = customPath;%0A            %7D%0A        %7D%0A        try %7B%0A            if (EXTERNAL_PATH != null) %7B%0A                WATCHER = EXTERNAL_PATH.register(FileSystems.getDefault().newWatchService(), StandardWatchEventKinds.ENTRY_MODIFY);%0A            %7D%0A        %7D catch (IOException ex) %7B%0A            LOG.error(%22Failed to initialize custom i18n messages watcher service%22);%0A        %7D%0A    %7D%0A%0A    private final ReentrantLock updateLock = new ReentrantLock();%0A    private Path externalResource;%0A    private Properties properties;%0A    private Date lastUpdate;%0A%0A    public CustomResourceBundle() %7B%0A        Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();%0A        if ((EXTERNAL_PATH != null) && (WATCHER != null)) %7B%0A            setParent(ResourceBundle.getBundle(BASE_NAME, locale, CONTROL));%0A        %7D else %7B%0A            setParent(ResourceBundle.getBundle(BASE_NAME, locale));%0A        %7D%0A        %0A        this.lastUpdate = new Date();%0A    %7D%0A%0A    private CustomResourceBundle(Path externalResource, Properties properties) %7B%0A        this.externalResource = externalResource;%0A        this.properties = properties;%0A        %0A        this.lastUpdate = new Date();%0A    %7D%0A%0A    @Override%0A    protected Object handleGetObject(String key) %7B%0A        if (properties != null) %7B%0A            checkWatcher();%0A            %0A            updateLock.lock();%0A            try %7B%0A                if (WATCHER_LAST_UPDATE.after(this.lastUpdate)) %7B%0A                    InputStream input = null;%0A                    try %7B%0A                        input = new FileInputStream(externalResource.toFile());%0A                        properties.load(input);%0A                    %7D catch (IOException ex) %7B%0A                        throw new IllegalStateException(ex);%0A                    %7D finally %7B%0A                        if (input != null) %7B%0A                            IOUtils.closeQuietly(input);%0A                        %7D%0A                    %7D%0A                    this.lastUpdate = new Date();%0A                %7D%0A            %7D finally %7B%0A                updateLock.unlock();%0A            %7D%0A%0A            return properties.get(key);%0A        %7D%0A%0A        return parent.getObject(key);%0A    %7D%0A%0A    private void checkWatcher() %7B%0A        if (!WATCHER.pollEvents().isEmpty()) %7B%0A            WATCHER_LAST_UPDATE = new Date();%0A        %7D%0A    %7D%0A%0A    @Override%0A    @SuppressWarnings(%7B %22rawtypes%22, %22unchecked%22 %7D)%0A    public Enumeration%3CString%3E getKeys() %7B%0A        if (properties != null) %7B%0A            Set keys = properties.keySet();%0A            return Collections.enumeration(keys);%0A        %7D%0A%0A        return parent.getKeys();%0A    %7D%0A%0A    protected static class CustomControl extends Control %7B%0A%0A        @Override%0A        public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)%0A                throws IllegalAccessException, InstantiationException, IOException %7B%0A            String resourceName = toResourceName(toBundleName(baseName, locale), %22properties%22);%0A            Path externalResource = EXTERNAL_PATH.resolve(resourceName);%0A            Properties properties = new Properties();%0A%0A            InputStream input = null;%0A            try %7B%0A                input = loader.getResourceAsStream(resourceName);%0A                properties.load(input); // Default (internal) bundle.%0A            %7D finally %7B%0A                if (input != null) %7B%0A                    IOUtils.closeQuietly(input);%0A                %7D%0A            %7D%0A%0A            input = null;%0A            try %7B%0A                File file = externalResource.toFile();%0A                if (file.exists()) %7B%0A                    input = new FileInputStream(externalResource.toFile());%0A                    properties.load(input); // External bundle (will overwrite same keys).%0A                %7D%0A            %7D finally %7B%0A                if (input != null) %7B%0A                    IOUtils.closeQuietly(input);%0A                %7D%0A            %7D%0A%0A            return new CustomResourceBundle(externalResource, properties);%0A        %7D%0A
+%0A    @Override%0A    public String getBaseName() %7B%0A        return BASE_NAME;
 %0A   

Allow to customize messages.properties #735