diff --git a/service/src/main/java/org/signal/i18n/HeaderControlledResourceBundleLookup.java b/service/src/main/java/org/signal/i18n/HeaderControlledResourceBundleLookup.java new file mode 100644 index 000000000..7e2660938 --- /dev/null +++ b/service/src/main/java/org/signal/i18n/HeaderControlledResourceBundleLookup.java @@ -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 getAcceptableLocales(final List acceptableLanguages) { + return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList()); + } + + @Nonnull + public ResourceBundle getResourceBundle(final String baseName, final List acceptableLocales) { + final List 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 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); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ResourceBundleFactory.java b/service/src/main/java/org/signal/i18n/ResourceBundleFactory.java similarity index 85% rename from service/src/main/java/org/whispersystems/textsecuregcm/badges/ResourceBundleFactory.java rename to service/src/main/java/org/signal/i18n/ResourceBundleFactory.java index 626dbb523..83bc7f283 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ResourceBundleFactory.java +++ b/service/src/main/java/org/signal/i18n/ResourceBundleFactory.java @@ -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; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java index 54857c98e..f92815708 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java @@ -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 knownBadges; private final List 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 acceptableLanguages, final String badgeId) { - final List 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 acceptableLocales = getAcceptableLocales(acceptableLanguages); - final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales); + final ResourceBundle resourceBundle = headerControlledResourceBundleLookup.getResourceBundle(BASE_NAME, + acceptableLanguages); List 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 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 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 getAcceptableLocales(final List acceptableLanguages) { - return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList()); - } - private Badge newBadge( final boolean isSelf, final String id, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java index d1e8056b8..c5fe27d41 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java @@ -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");