com.carlomicieli.jtrains.value.objects.LocalizedField.java Source code

Java tutorial

Introduction

Here is the source code for com.carlomicieli.jtrains.value.objects.LocalizedField.java

Source

/*
 * Copyright 2014 the original author or authors.
 *
 *  Licensed under the Apache License, Version 2.0 (the 'License');
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an 'AS IS' BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.carlomicieli.jtrains.value.objects;

import com.carlomicieli.jtrains.AppGlobals;
import com.carlomicieli.jtrains.validation.ISOValidationUtils;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializable;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.google.common.collect.ImmutableMap;

import java.io.IOException;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * It represents a container for different value for the same field, every
 * value is specific to a particular {@code Locale}.
 *
 *  <p>
 *      <pre>
 *  LocalizedField<String> enLabel = LocalizedField.of("English, by default");
 *  String en = enLabel.get(Locale.ENGLISH).orElse(null);   // yields "English, by default"
 *
 *  LocalizedField<String> frLabel = LocalizedField.of(Locale.FRENCH, "Joyeaux Noel");
 *  String fr = frLabel.get(Locale.FRENCH).orElse(null);   // yields "Joyeaux Noel"
 *  String de = frLabel.get(Locale.GERMAN).orElse(LocalizedField.defaultSupplier());    // calls the default supplier
 *      </pre>
 *  </p>
 *  <p>
 *      {@code LocalizedField}s are immutable objects, methods that attempt to change the current {@code LocalizedField}
 *      are actually returning new objects.
 *
 *      <pre>
 *  LocalizedField<String> enAndFrLabel = enLabel.with(Locale.FRENCH, "Joyeaux Noel");
 *  assertThat(enAndFrLabel)
 *          .containsEntry(Locale.ENGLISH, "English, by default")
 *          .containsEntry(Locale.FRENCH, "Joyeaux Noel");
 *      </pre>
 *  </p>
 *  <p>
 *      This class can be {@code serialized} into Json. As this class is immutable is therefore thread-safe.
 *  </p>
 *
 * @author Carlo Micieli
 * @since 1.0
 *
 * @param <T> the type of the localized element
 */
public final class LocalizedField<T> implements JsonSerializable {

    private final Map<String, T> values;

    @JsonCreator
    private LocalizedField(Map<String, T> values) {
        this.values = validateValues(values);
    }

    /**
     * Returns the size of the current {@code LocalizedField}.
     * @return the size.
     */
    public int size() {
        return this.values.size();
    }

    /**
     * Checks whether the current field has default value.
     * @return {@code true} if contains a default value; {@code false} otherwise
     */
    public boolean containsDefault() {
        return this.values.containsKey(AppGlobals.DEFAULT_LANGUAGE);
    }

    /**
     * Checks whether the current field has a value for the provided {@code Locale}.
     * @param locale the {@code Locale}
     * @return {@code true} if contains a value; {@code false} otherwise
     */
    public boolean contains(Locale locale) {
        return this.values.containsKey(keyFromLocale(locale));
    }

    /**
     * Returns the localized value for the provided {@code Locale}.
     * @param lang the {@code Locale}
     * @return the value to which the specified {@code Locale} is mapped;
     *          or {@code Optional#empty()} if no value is mapped with the {@code Locale}
     */
    public Optional<T> get(Locale lang) {
        return Optional.ofNullable(values.get(keyFromLocale(lang)));
    }

    /**
     * Returns the localized value for the provided {@code Locale} if found, if
     * such value is not found then it will try a second attempt with the default
     * language.
     *
     * @param lang the {@code Locale}
     * @return the value to which the specified {@code Locale} is mapped;
     *          or {@code null} if no value is mapped with the {@code Locale}
     */
    public T getOrDefault(Locale lang) {
        return Optional.ofNullable(values.get(keyFromLocale(lang))).orElseGet(defaultSupplier());
    }

    /**
     * Returns the {@code Set} of the {@code Locale}s used in the current field.
     * @return the {@code Locale}s set
     */
    public Set<Locale> locales() {
        return this.values.keySet().stream().map(LocalizedField::localeFromKey).collect(Collectors.toSet());
    }

    /**
     * Creates a new {@code LocalizedField} with the provided value assigned
     * to the default {@code Locale}.
     *
     * @param value the value
     * @return a {@code LocalizedField}
     */
    public static <T> LocalizedField<T> of(T value) {
        Objects.requireNonNull(value);
        String defaultLocale = keyFromLocale(AppGlobals.DEFAULT_LOCALE);
        return new LocalizedField<>(ImmutableMap.of(defaultLocale, value));
    }

    /**
     * Creates a new {@code LocalizedField} with the provided value and {@code Locale}.
     * @param value the value
     * @return a {@code LocalizedField}
     */
    public static <T> LocalizedField<T> of(Locale lang, T value) {
        Objects.requireNonNull(lang);
        Objects.requireNonNull(value);

        String key = keyFromLocale(lang);
        return new LocalizedField<>(ImmutableMap.of(key, value));
    }

    /**
     * Builds a new {@code LocalizedField} using the value from the provided map.
     * <p>
     *     The method returns {@code null} if the provided map is either null or
     *     empty.
     * </p>
     *
     * @param values the map
     * @return a {@code LocalizedField}
     */
    public static <T> LocalizedField<T> of(Map<String, T> values) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        return new LocalizedField<>(values);
    }

    /**
     * Returns the default {@code Supplier} for the current {@code LocalizedField} object.
     * @return the {@code Supplier} producing the default value.
     */
    public Supplier<T> defaultSupplier() {
        return () -> this.values.get(AppGlobals.DEFAULT_LANGUAGE);
    }

    /**
     * Return a sub map for the provided list of {@code Locale}s.
     * <p>
     *     This method will return an immutable {@code Map} with an item for
     *     each of the provided {@code Locale}. When the appropriate localized message
     *     is not found, the default value is returned instead.
     * </p>
     * <p>
     *     The result {@code Map} is not preserving the order of the arguments.
     * </p>
     *
     * @param locales a list of {@code Locale}s
     * @return a sub map of localized value
     */
    public Map<Locale, T> getValues(Locale... locales) {
        Map<Locale, T> map = new HashMap<>();

        for (Locale l : locales) {
            map.put(l, this.get(l).orElse(null));
        }

        return Collections.unmodifiableMap(map);
    }

    /**
     * Returns {@code true} if the provided {@code Locale} is the default for
     * the current instance.
     *
     * @param locale the {@code Locale}
     * @return {@code true} if the provided {@code Locale} is the default for
     *         the current instance.
     */
    public static boolean isDefaultLocale(Locale locale) {
        return locale.equals(AppGlobals.DEFAULT_LOCALE);
    }

    /**
     * Adds a new value to this {@code LocalizedField} for the default language
     * and return a brand new object.
     *
     * @param value the new value
     * @return a new {@code LocalizedField} object
     */
    public LocalizedField<T> with(T value) {
        return with(AppGlobals.DEFAULT_LOCALE, value);
    }

    /**
     * Adds a new value to this {@code LocalizedField} and return a brand new object.
     * @param locale the {@code Locale} for the new value
     * @param value the new value
     * @return a new {@code LocalizedField} object
     */
    public LocalizedField<T> with(Locale locale, T value) {
        Objects.requireNonNull(locale, "LocalizedField: locale is required");
        Objects.requireNonNull(value, "LocalizedField: value is required");

        Map<String, T> map = new HashMap<>();
        map.put(keyFromLocale(locale), value);
        map.putAll(values);
        return new LocalizedField<>(map);
    }

    @Override
    public String toString() {
        return this.values.toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof LocalizedField<?>))
            return false;
        LocalizedField<?> that = (LocalizedField<?>) obj;
        return Objects.equals(this.values, that.values);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.values);
    }

    /**
     * Returns the current {@code LocalizedField} as a Java {@code Map}.
     * @return a Java {@code Map}.
     */
    public Map<String, T> toMap() {
        return values;
    }

    private static String keyFromLocale(Locale locale) {
        return locale.getLanguage();
    }

    private static Locale localeFromKey(String key) {
        return new Locale.Builder().setLanguage(key).build();
    }

    private static <V> Map<String, V> validateValues(Map<String, V> values) {
        for (String locale : values.keySet()) {
            sanitizeLanguage(locale);
        }

        return ImmutableMap.copyOf(values);
    }

    private static String sanitizeLanguage(String lang) {
        Locale l = Locale.forLanguageTag(lang);

        if (!ISOValidationUtils.countryIsValid(l.getCountry())) {
            throw new IllegalArgumentException("'" + lang + "' is not a valid locale code (country).");
        }
        if (!ISOValidationUtils.languageIsValid(l.getLanguage())) {
            throw new IllegalArgumentException("'" + lang + "' is not a valid locale code (language).");
        }

        return l.getLanguage();
    }

    @Override
    public void serialize(JsonGenerator json, SerializerProvider provider) throws IOException {
        json.writeObject(values);
    }

    @Override
    public void serializeWithType(JsonGenerator json, SerializerProvider provider, TypeSerializer typeSer)
            throws IOException {
        json.writeObject(values);
    }
}