Extract logic for created header controlled resource bundles
This commit is contained in:
		
							parent
							
								
									f5a539e128
								
							
						
					
					
						commit
						94bf3a3902
					
				| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.signal.i18n;
 | 
			
		||||
 | 
			
		||||
import com.google.common.annotations.VisibleForTesting;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.ResourceBundle;
 | 
			
		||||
import java.util.ResourceBundle.Control;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
 | 
			
		||||
public class HeaderControlledResourceBundleLookup {
 | 
			
		||||
 | 
			
		||||
  private static final int MAX_LOCALES = 15;
 | 
			
		||||
 | 
			
		||||
  private final ResourceBundleFactory resourceBundleFactory;
 | 
			
		||||
 | 
			
		||||
  public HeaderControlledResourceBundleLookup() {
 | 
			
		||||
    this(ResourceBundle::getBundle);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @VisibleForTesting
 | 
			
		||||
  public HeaderControlledResourceBundleLookup(
 | 
			
		||||
      @Nonnull final ResourceBundleFactory resourceBundleFactory) {
 | 
			
		||||
    this.resourceBundleFactory = Objects.requireNonNull(resourceBundleFactory);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Nonnull
 | 
			
		||||
  private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
 | 
			
		||||
    return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Nonnull
 | 
			
		||||
  public ResourceBundle getResourceBundle(final String baseName, final List<Locale> acceptableLocales) {
 | 
			
		||||
    final List<Locale> deduplicatedLocales = getAcceptableLocales(acceptableLocales);
 | 
			
		||||
    final Locale desiredLocale = deduplicatedLocales.isEmpty() ? Locale.getDefault() : deduplicatedLocales.get(0);
 | 
			
		||||
    // define a control with a fallback order as specified in the header
 | 
			
		||||
    Control control = new Control() {
 | 
			
		||||
      @Override
 | 
			
		||||
      public List<String> getFormats(final String baseName) {
 | 
			
		||||
        Objects.requireNonNull(baseName);
 | 
			
		||||
        return Control.FORMAT_PROPERTIES;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      @Override
 | 
			
		||||
      public Locale getFallbackLocale(final String baseName, final Locale locale) {
 | 
			
		||||
        Objects.requireNonNull(baseName);
 | 
			
		||||
        if (locale.equals(Locale.getDefault())) {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
        final int localeIndex = deduplicatedLocales.indexOf(locale);
 | 
			
		||||
        if (localeIndex < 0 || localeIndex >= deduplicatedLocales.size() - 1) {
 | 
			
		||||
          return Locale.getDefault();
 | 
			
		||||
        }
 | 
			
		||||
        // [0, deduplicatedLocales.size() - 2] is now the possible range for localeIndex
 | 
			
		||||
        return deduplicatedLocales.get(localeIndex + 1);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return resourceBundleFactory.createBundle(baseName, desiredLocale, control);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.whispersystems.textsecuregcm.badges;
 | 
			
		||||
package org.signal.i18n;
 | 
			
		||||
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.ResourceBundle;
 | 
			
		||||
| 
						 | 
				
			
			@ -13,12 +13,10 @@ import java.util.ArrayList;
 | 
			
		|||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.ResourceBundle;
 | 
			
		||||
import java.util.ResourceBundle.Control;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
 | 
			
		||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
 | 
			
		||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
 | 
			
		||||
import org.whispersystems.textsecuregcm.entities.Badge;
 | 
			
		||||
| 
						 | 
				
			
			@ -28,37 +26,36 @@ import org.whispersystems.textsecuregcm.storage.AccountBadge;
 | 
			
		|||
 | 
			
		||||
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, BadgeTranslator {
 | 
			
		||||
 | 
			
		||||
  private static final int MAX_LOCALES = 15;
 | 
			
		||||
  @VisibleForTesting
 | 
			
		||||
  static final String BASE_NAME = "org.signal.badges.Badges";
 | 
			
		||||
 | 
			
		||||
  private final Clock clock;
 | 
			
		||||
  private final Map<String, BadgeConfiguration> knownBadges;
 | 
			
		||||
  private final List<String> badgeIdsEnabledForAll;
 | 
			
		||||
  private final ResourceBundleFactory resourceBundleFactory;
 | 
			
		||||
  private final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup;
 | 
			
		||||
 | 
			
		||||
  public ConfiguredProfileBadgeConverter(
 | 
			
		||||
      final Clock clock,
 | 
			
		||||
      final BadgesConfiguration badgesConfiguration) {
 | 
			
		||||
    this(clock, badgesConfiguration, ResourceBundle::getBundle);
 | 
			
		||||
    this(clock, badgesConfiguration, new HeaderControlledResourceBundleLookup());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @VisibleForTesting
 | 
			
		||||
  public ConfiguredProfileBadgeConverter(
 | 
			
		||||
      final Clock clock,
 | 
			
		||||
      final BadgesConfiguration badgesConfiguration,
 | 
			
		||||
      final ResourceBundleFactory resourceBundleFactory) {
 | 
			
		||||
      final HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup) {
 | 
			
		||||
    this.clock = clock;
 | 
			
		||||
    this.knownBadges = badgesConfiguration.getBadges().stream()
 | 
			
		||||
        .collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity()));
 | 
			
		||||
    this.badgeIdsEnabledForAll = badgesConfiguration.getBadgeIdsEnabledForAll();
 | 
			
		||||
    this.resourceBundleFactory = resourceBundleFactory;
 | 
			
		||||
    this.headerControlledResourceBundleLookup = headerControlledResourceBundleLookup;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public Badge translate(final List<Locale> acceptableLanguages, final String badgeId) {
 | 
			
		||||
    final List<Locale> acceptableLocales = getAcceptableLocales(acceptableLanguages);
 | 
			
		||||
    final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales);
 | 
			
		||||
    final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
 | 
			
		||||
        acceptableLanguages);
 | 
			
		||||
    final BadgeConfiguration configuration = knownBadges.get(badgeId);
 | 
			
		||||
    return newBadge(
 | 
			
		||||
        false,
 | 
			
		||||
| 
						 | 
				
			
			@ -83,8 +80,8 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, B
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    final Instant now = clock.instant();
 | 
			
		||||
    final List<Locale> acceptableLocales = getAcceptableLocales(acceptableLanguages);
 | 
			
		||||
    final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales);
 | 
			
		||||
    final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME,
 | 
			
		||||
        acceptableLanguages);
 | 
			
		||||
    List<Badge> badges = accountBadges.stream()
 | 
			
		||||
        .filter(accountBadge -> (isSelf || accountBadge.isVisible())
 | 
			
		||||
            && now.isBefore(accountBadge.getExpiration())
 | 
			
		||||
| 
						 | 
				
			
			@ -121,40 +118,6 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, B
 | 
			
		|||
    return badges;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Nonnull
 | 
			
		||||
  private ResourceBundle getResourceBundle(final List<Locale> acceptableLocales) {
 | 
			
		||||
    final Locale desiredLocale = acceptableLocales.isEmpty() ? Locale.getDefault() : acceptableLocales.get(0);
 | 
			
		||||
    // define a control with a fallback order as specified in the header
 | 
			
		||||
    Control control = new Control() {
 | 
			
		||||
      @Override
 | 
			
		||||
      public List<String> getFormats(final String baseName) {
 | 
			
		||||
        Objects.requireNonNull(baseName);
 | 
			
		||||
        return Control.FORMAT_PROPERTIES;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      @Override
 | 
			
		||||
      public Locale getFallbackLocale(final String baseName, final Locale locale) {
 | 
			
		||||
        Objects.requireNonNull(baseName);
 | 
			
		||||
        if (locale.equals(Locale.getDefault())) {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
        final int localeIndex = acceptableLocales.indexOf(locale);
 | 
			
		||||
        if (localeIndex < 0 || localeIndex >= acceptableLocales.size() - 1) {
 | 
			
		||||
          return Locale.getDefault();
 | 
			
		||||
        }
 | 
			
		||||
        // [0, acceptableLocales.size() - 2] is now the possible range for localeIndex
 | 
			
		||||
        return acceptableLocales.get(localeIndex + 1);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return resourceBundleFactory.createBundle(BASE_NAME, desiredLocale, control);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Nonnull
 | 
			
		||||
  private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
 | 
			
		||||
    return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private Badge newBadge(
 | 
			
		||||
      final boolean isSelf,
 | 
			
		||||
      final String id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,8 @@ import org.junit.jupiter.params.ParameterizedTest;
 | 
			
		|||
import org.junit.jupiter.params.provider.Arguments;
 | 
			
		||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
			
		||||
import org.mockito.ArgumentCaptor;
 | 
			
		||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
 | 
			
		||||
import org.signal.i18n.ResourceBundleFactory;
 | 
			
		||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
 | 
			
		||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
 | 
			
		||||
import org.whispersystems.textsecuregcm.entities.Badge;
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +68,8 @@ public class ConfiguredProfileBadgeConverterTest {
 | 
			
		|||
 | 
			
		||||
  private static BadgeConfiguration newBadge(int i) {
 | 
			
		||||
    return new BadgeConfiguration(
 | 
			
		||||
        idFor(i), "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")));
 | 
			
		||||
        idFor(i), "other", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
			
		||||
        List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private BadgesConfiguration createBadges(int count) {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +107,7 @@ public class ConfiguredProfileBadgeConverterTest {
 | 
			
		|||
  void testConvertEmptyList() {
 | 
			
		||||
    BadgesConfiguration badgesConfiguration = createBadges(1);
 | 
			
		||||
    ConfiguredProfileBadgeConverter badgeConverter = new ConfiguredProfileBadgeConverter(clock, badgesConfiguration,
 | 
			
		||||
        resourceBundleFactory);
 | 
			
		||||
        new HeaderControlledResourceBundleLookup(resourceBundleFactory));
 | 
			
		||||
    assertThat(badgeConverter.convert(List.of(Locale.getDefault()), List.of(), false)).isNotNull().isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +116,8 @@ public class ConfiguredProfileBadgeConverterTest {
 | 
			
		|||
  void testNoLocales(String name, Instant expiration, boolean visible, boolean isSelf, Badge expectedBadge) {
 | 
			
		||||
    BadgesConfiguration badgesConfiguration = createBadges(1);
 | 
			
		||||
    ConfiguredProfileBadgeConverter badgeConverter =
 | 
			
		||||
        new ConfiguredProfileBadgeConverter(clock, badgesConfiguration, resourceBundleFactory);
 | 
			
		||||
        new ConfiguredProfileBadgeConverter(clock, badgesConfiguration,
 | 
			
		||||
            new HeaderControlledResourceBundleLookup(resourceBundleFactory));
 | 
			
		||||
    setupResourceBundle(Locale.getDefault());
 | 
			
		||||
 | 
			
		||||
    if (expectedBadge != null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -136,15 +140,26 @@ public class ConfiguredProfileBadgeConverterTest {
 | 
			
		|||
        arguments(idFor(0), expired, false, false, null),
 | 
			
		||||
        arguments(idFor(0), notExpired, false, false, null),
 | 
			
		||||
        arguments(idFor(0), expired, true, false, null),
 | 
			
		||||
        arguments(idFor(0), notExpired, true, false, new Badge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")))),
 | 
			
		||||
        arguments(idFor(0), notExpired, true, false,
 | 
			
		||||
            new Badge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
			
		||||
                List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"),
 | 
			
		||||
                    new BadgeSvg("ll", "ld", "lt")))),
 | 
			
		||||
        arguments(idFor(1), expired, false, false, null),
 | 
			
		||||
        arguments(idFor(1), notExpired, false, false, null),
 | 
			
		||||
        arguments(idFor(1), expired, true, false, null),
 | 
			
		||||
        arguments(idFor(1), notExpired, true, false, null),
 | 
			
		||||
        arguments(idFor(0), expired, false, true, null),
 | 
			
		||||
        arguments(idFor(0), notExpired, false, true, new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")), notExpired, false)),
 | 
			
		||||
        arguments(idFor(0), notExpired, false, true,
 | 
			
		||||
            new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"),
 | 
			
		||||
                "SVG",
 | 
			
		||||
                List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")),
 | 
			
		||||
                notExpired, false)),
 | 
			
		||||
        arguments(idFor(0), expired, true, true, null),
 | 
			
		||||
        arguments(idFor(0), notExpired, true, true, new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")), notExpired, true)),
 | 
			
		||||
        arguments(idFor(0), notExpired, true, true,
 | 
			
		||||
            new SelfBadge(idFor(0), "other", nameFor(0), desriptionFor(0), List.of("l", "m", "h", "x", "xx", "xxx"),
 | 
			
		||||
                "SVG",
 | 
			
		||||
                List.of(new BadgeSvg("sl", "sd", "st"), new BadgeSvg("ml", "md", "mt"), new BadgeSvg("ll", "ld", "lt")),
 | 
			
		||||
                notExpired, true)),
 | 
			
		||||
        arguments(idFor(1), expired, false, true, null),
 | 
			
		||||
        arguments(idFor(1), notExpired, false, true, null),
 | 
			
		||||
        arguments(idFor(1), expired, true, true, null),
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +170,8 @@ public class ConfiguredProfileBadgeConverterTest {
 | 
			
		|||
  void testCustomControl() {
 | 
			
		||||
    BadgesConfiguration badgesConfiguration = createBadges(1);
 | 
			
		||||
    ConfiguredProfileBadgeConverter badgeConverter =
 | 
			
		||||
        new ConfiguredProfileBadgeConverter(clock, badgesConfiguration, resourceBundleFactory);
 | 
			
		||||
        new ConfiguredProfileBadgeConverter(clock, badgesConfiguration,
 | 
			
		||||
            new HeaderControlledResourceBundleLookup(resourceBundleFactory));
 | 
			
		||||
 | 
			
		||||
    Locale defaultLocale = Locale.getDefault();
 | 
			
		||||
    Locale enGb = new Locale("en", "GB");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue