AbstractI18nContextProvider.java

package dev.orne.i18n.context;

/*-
 * #%L
 * Orne I18N
 * %%
 * Copyright (C) 2022 Orne Developments
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.function.Supplier;

import javax.validation.constraints.NotNull;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;

import dev.orne.i18n.I18nResources;

/**
 * Abstract implementation of {@code I18nContextProvider}.
 * 
 * @author <a href="https://github.com/ihernaez">(w) Iker Hernaez</a>
 * @version 1.0, 2022-12
 * @see I18nContextProvider
 * @since 0.1
 */
@API(status=Status.STABLE, since="0.1")
public abstract class AbstractI18nContextProvider
implements I18nContextProvider {

    /** The UUID of this provider instance. */
    private @NotNull UUID sessionUUID = UUID.randomUUID();
    /** The default locale supplier. */
    private final @NotNull Supplier<@NotNull Locale> defaultLocaleSupplier;
    /** The available locales. */
    private final @NotNull @NotNull Locale[] availableLocales;
    /** The default I18N resources. */
    private final @NotNull I18nResources defaultI18nResources;
    /** The alternative I18N resources by key. */
    private final @NotNull Map<@NotNull String, @NotNull I18nResources> i18nResources;

    /**
     * Creates a new instance based on specified builder.
     * 
     * @param builder The I18N context provider builder.
     */
    protected AbstractI18nContextProvider(
            final @NotNull BuilderImpl<?, ?> builder) {
        super();
        this.defaultLocaleSupplier = builder.defaultLocaleSupplier;
        this.availableLocales = builder.availableLocales;
        this.defaultI18nResources = builder.defaultI18nResources;
        this.i18nResources = Collections.unmodifiableMap(
                new HashMap<>(builder.i18nResources));
    }

    /**
     * Returns the UUID of this provider instance and session.
     * Used to check contexts validity. Constant from instance creation to
     * call to {@code invalidate()}
     * 
     * @return The UUID of this provider instance
     */
    public @NotNull UUID getSessionUUID() {
        return this.sessionUUID;
    }

    /**
     * Returns the default locale supplier.
     * 
     * @return The default locale supplier.
     */
    protected @NotNull Supplier<@NotNull Locale> getDefaultLocaleSupplier() {
        return this.defaultLocaleSupplier;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull Locale[] getAvailableLocales() {
        return Arrays.copyOf(availableLocales, availableLocales.length);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull I18nResources getDefaultI18nResources() {
        return this.defaultI18nResources;
    }

    /**
     * Returns the alternative I18N resources by key.
     * 
     * @return The alternative I18N resources by key
     */
    public @NotNull Map<@NotNull String, @NotNull I18nResources> getI18nResources() {
        return Collections.unmodifiableMap(this.i18nResources);
    }

    /**
     * Returns the I18N resources identified by the specified key.
     * If key is {@code null} or no resources is associated for such key
     * returns the default I18N resources.
     * 
     * @param key The key of the alternative I18N resources
     * @return The I18N resources to use for the key
     */
    public @NotNull I18nResources getI18nResources(
            final String key) {
        if (key == null) {
            return this.defaultI18nResources;
        } else {
            return this.i18nResources.getOrDefault(key, this.defaultI18nResources);
        }
    }

    /**
     * Creates a new I18N context with default values.
     * 
     * @return The new I18N context
     */
    public @NotNull I18nContext createContext() {
        final I18nContext context = new DefaultI18nContext(this.sessionUUID);
        context.setLocale(getDefaultLocaleSupplier().get());
        return context;
    }

    /**
     * Creates a new I18N context with values inherited from the specified
     * parent I18N context.
     * 
     * @param parent The parent I18N context
     * @return The new I18N context
     */
    public @NotNull I18nContext createContext(
            final @NotNull I18nContext parent) {
        Validate.notNull(parent);
        final I18nContext context = createContext();
        context.setLocale(parent.getLocale());
        return context;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation resets available languages, and I18N resources to
     * defaults and generates a new session UUID to invalidate any existing
     * contexts.
     */
    @Override
    public synchronized void invalidate() {
        this.sessionUUID = UUID.randomUUID();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(this.defaultLocaleSupplier.get())
                .append(this.availableLocales)
                .append(this.defaultI18nResources)
                .append(this.i18nResources)
                .toHashCode();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == null) { return false; }
        if (obj == this) { return true; }
        if (!getClass().equals(obj.getClass())) { return false; }
        final AbstractI18nContextProvider other = (AbstractI18nContextProvider) obj;
        return new EqualsBuilder()
                .append(this.defaultLocaleSupplier.get(), other.defaultLocaleSupplier.get())
                .append(this.availableLocales, other.availableLocales)
                .append(this.defaultI18nResources, other.defaultI18nResources)
                .append(this.i18nResources, other.i18nResources)
                .isEquals();
    }

    /**
     * Abstract builder of I18N context provider instances.
     * 
     * @author <a href="https://github.com/ihernaez">(w) Iker Hernaez</a>
     * @version 1.0, 2024-09
     * @param <T> The type of I18N context provider build by the builder.
     * @param <B> The type of builder returned for method chaining.
     * @since 0.1
     */
    protected abstract static class BuilderImpl<
            T extends AbstractI18nContextProvider,
            B extends BuilderImpl<T, B>> {

        /** The default language supplier. */
        protected @NotNull Supplier<@NotNull Locale> defaultLocaleSupplier =
                Locale::getDefault;
        /** The supported languages. */
        protected @NotNull Locale[] availableLocales =
                Locale.getAvailableLocales();
        /** The default I18N resources. */
        protected @NotNull I18nResources defaultI18nResources =
                DummyI18nResources.getInstance();
        /** The alternative I18N resources by key. */
        protected final @NotNull Map<@NotNull String, @NotNull I18nResources> i18nResources =
                new HashMap<>();

        /**
         * Creates a new instance.
         */
        protected BuilderImpl() {
            super();
        }

        /**
         * Configures the builder with specified I18N configuration.
         * 
         * @param config The I18N configuration.
         * @return This builder, for method chaining.
         * @see I18nContextProvider.Builder#configure(Properties)
         */
        @SuppressWarnings("unchecked")
        public @NotNull B configure(
                @NotNull Properties config) {
            configureDefaultLocaleSupplier(config);
            configureAvailableLocalesSupplier(config);
            configureDefaultI18nResources(config);
            configureAlternativeI18nResources(config);
            return (B) this;
        }

        /**
         * Configures the default language based on specified configuration.
         * 
         * @param config The I18N configuration.
         */
        protected void configureDefaultLocaleSupplier(
                final @NotNull Properties config) {
            if (config.containsKey(I18nConfiguration.DEFAULT_LANGUAGE)) {
                final Locale locale = new Locale(config.getProperty(I18nConfiguration.DEFAULT_LANGUAGE));
                setDefaultLocaleSupplier(() -> locale);
            }
        }

        /**
         * Configures the supported languages based on specified configuration.
         * 
         * @param config The I18N configuration.
         */
        protected void configureAvailableLocalesSupplier(
                final @NotNull Properties config) {
            if (config.containsKey(I18nConfiguration.AVAILABLE_LANGUAGES)) {
                final String[] langs = StringUtils.split(
                        config.getProperty(I18nConfiguration.AVAILABLE_LANGUAGES),
                        ",");
                final Locale[] locales = new Locale[langs.length];
                for (int i = 0; i < langs.length; i++) {
                    locales[i] = new Locale(langs[i]);
                }
                setAvailableLocales(locales);
            }
        }

        /**
         * Configures the default I18N resources based on specified configuration.
         * 
         * @param config The I18N configuration.
         */
        protected void configureDefaultI18nResources(
                final @NotNull Properties config) {
            if (config.containsKey(I18nConfiguration.DEFAULT_RESOURCES)) {
                setDefaultI18nResources(I18nBundleResources.forBasename(
                        config.getProperty(I18nConfiguration.DEFAULT_RESOURCES)));
            }
        }

        /**
         * Configures the alternative I18N resources based on specified configuration.
         * 
         * @param config The I18N configuration.
         */
        protected void configureAlternativeI18nResources(
                final @NotNull Properties config) {
            for (final String prop : config.stringPropertyNames()) {
                if (prop.startsWith(I18nConfiguration.NAMED_RESOURCES_PREFIX)) {
                    final String resourceName = prop.substring(I18nConfiguration.NAMED_RESOURCES_PREFIX.length());
                    addI18nResources(
                            resourceName,
                            I18nBundleResources.forBasename(config.getProperty(prop)));
                }
            }
        }

        /**
         * Sets the default locale supplier.
         * 
         * @param supplier The default locale supplier.
         * @return This instance, for method chaining.
         * @see I18nContextProvider.Builder#setDefaultLocaleSupplier(Supplier)
         */
        @SuppressWarnings("unchecked")
        public @NotNull B setDefaultLocaleSupplier(
                final @NotNull Supplier<@NotNull Locale> supplier) {
            this.defaultLocaleSupplier = supplier;
            return (B) this;
        }

        /**
         * Sets the available locales.
         * 
         * @param locales The available locales.
         * @return This instance, for method chaining.
         * @see I18nContextProvider.Builder#setAvailableLocales(Locale[])
         */
        @SuppressWarnings("unchecked")
        public @NotNull B setAvailableLocales(
                final @NotNull Locale[] locales) {
            this.availableLocales = Validate.notNull(locales);
            return (B) this;
        }

        /**
         * Sets the default I18N resources.
         * 
         * @param resources The default I18N resources
         * @return This instance, for method chaining.
         * @see I18nContextProvider.Builder#setDefaultI18nResources(I18nResources)
         */
        @SuppressWarnings("unchecked")
        public @NotNull B setDefaultI18nResources(
                final @NotNull I18nResources resources) {
            this.defaultI18nResources = Validate.notNull(resources);
            return (B) this;
        }

        /**
         * Adds alternative I18N resources to be used when the specified key is
         * used.
         * 
         * @param key The key of the alternative I18N resources
         * @param resource The alternative I18N resources
         * @return This instance, for method chaining.
         * @see I18nContextProvider.Builder#addI18nResources(String, I18nResources)
         */
        @SuppressWarnings("unchecked")
        public @NotNull B addI18nResources(
                final @NotNull String key,
                final @NotNull I18nResources resource) {
            this.i18nResources.put(
                    Validate.notNull(key),
                    Validate.notNull(resource));
            return (B) this;
        }
    }
}