Extract logic for created header controlled resource bundles

This commit is contained in:
Ehren Kret 2021-10-28 14:26:53 -07:00
parent f5a539e128
commit 94bf3a3902
4 changed files with 100 additions and 54 deletions

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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");