Skip to content

Commit

Permalink
Add tests and documentation. Improve edge case handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
pmendelski committed Jan 4, 2023
1 parent 166cd11 commit 6460110
Show file tree
Hide file tree
Showing 58 changed files with 2,006 additions and 539 deletions.
406 changes: 379 additions & 27 deletions README.md

Large diffs are not rendered by default.

35 changes: 18 additions & 17 deletions src/main/java/com/coditory/quark/i18n/AggregatedI18nLoader.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.coditory.quark.i18n;

import com.coditory.quark.i18n.loader.I18nLoader;
import com.coditory.quark.i18n.loader.I18nTemplatesBundle;
import com.coditory.quark.i18n.loader.I18nMessageBundle;
import com.coditory.quark.i18n.loader.WatchableI18nLoader;
import org.jetbrains.annotations.NotNull;

Expand All @@ -13,29 +13,29 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.Objects.requireNonNull;
import static com.coditory.quark.i18n.Preconditions.expectNonNull;

final class AggregatedI18nLoader implements WatchableI18nLoader {
private final List<I18nLoader> loaders = new ArrayList<>();
private final Map<I18nKey, String> currentEntries = new LinkedHashMap<>();
private final ConcurrentHashMap<I18nLoader, List<I18nTemplatesBundle>> cachedResults = new ConcurrentHashMap<>();
private final ConcurrentHashMap<I18nLoader, List<I18nMessageBundle>> cachedResults = new ConcurrentHashMap<>();
private final Set<I18nLoaderChangeListener> listeners = new LinkedHashSet<>();
private boolean watching = false;

public synchronized void addLoader(I18nLoader loader) {
requireNonNull(loader);
expectNonNull(loader, "loader");
appendCurrentEntries();
loaders.add(loader);
}

public synchronized void addMessage(I18nKey key, String value) {
requireNonNull(key);
requireNonNull(value);
expectNonNull(key, "key");
expectNonNull(value, "value");
currentEntries.put(key, value);
}

public synchronized void addMessages(Map<I18nKey, String> messages) {
requireNonNull(messages);
expectNonNull(messages, "messages");
currentEntries.putAll(messages);
}

Expand All @@ -48,6 +48,7 @@ public synchronized AggregatedI18nLoader copy() {

@Override
public synchronized void addChangeListener(I18nLoaderChangeListener listener) {
expectNonNull(listener, "listener");
listeners.add(listener);
}

Expand All @@ -60,14 +61,14 @@ public synchronized void startWatching() {
for (I18nLoader loader : loaders) {
if (loader instanceof WatchableI18nLoader watchableLoader) {
watchableLoader.startWatching();
watchableLoader.addChangeListener(entries -> onBundlesChange(loader, entries));
watchableLoader.addChangeListener(bundles -> onBundlesChange(loader, bundles));
}
}
}

private synchronized void onBundlesChange(I18nLoader loader, List<I18nTemplatesBundle> bundles) {
private synchronized void onBundlesChange(I18nLoader loader, List<I18nMessageBundle> bundles) {
cachedResults.put(loader, bundles);
List<I18nTemplatesBundle> result = loaders.stream()
List<I18nMessageBundle> result = loaders.stream()
.map(l -> cachedResults.getOrDefault(l, List.of()))
.reduce(new ArrayList<>(), (m, e) -> {
m.addAll(e);
Expand All @@ -92,7 +93,7 @@ public synchronized void stopWatching() {

@Override
@NotNull
public synchronized List<I18nTemplatesBundle> load() {
public synchronized List<I18nMessageBundle> load() {
appendCurrentEntries();
return loaders.stream()
.map(this::load)
Expand All @@ -102,17 +103,17 @@ public synchronized List<I18nTemplatesBundle> load() {
});
}

private List<I18nTemplatesBundle> load(I18nLoader loader) {
List<I18nTemplatesBundle> entries = loader.load();
cachedResults.put(loader, entries);
return entries;
private List<I18nMessageBundle> load(I18nLoader loader) {
List<I18nMessageBundle> bundles = loader.load();
cachedResults.put(loader, bundles);
return bundles;
}

private void appendCurrentEntries() {
if (!currentEntries.isEmpty()) {
Map<I18nKey, String> copy = new LinkedHashMap<>(currentEntries);
I18nTemplatesBundle templates = new I18nTemplatesBundle(copy);
List<I18nTemplatesBundle> result = List.of(templates);
I18nMessageBundle templates = new I18nMessageBundle(copy);
List<I18nMessageBundle> result = List.of(templates);
I18nLoader loader = () -> result;
loaders.add(loader);
cachedResults.put(loader, result);
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/com/coditory/quark/i18n/ArgumentIndexExtractor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.coditory.quark.i18n;

import java.util.HashSet;
import java.util.PrimitiveIterator;
import java.util.Set;

import static com.coditory.quark.i18n.Preconditions.expectNonNull;
import static java.util.Collections.unmodifiableSet;

final class ArgumentIndexExtractor {
static Set<Integer> extractArgumentIndexes(String template) {
expectNonNull(template, "template");
Set<Integer> result = new HashSet<>();
boolean escaped = false;
int stack = 0;
int prev = -1;
for (PrimitiveIterator.OfInt it = template.codePoints().iterator(); it.hasNext(); ) {
int c = it.next();
if (c == '\'') {
escaped = true;
} else if (c == '{' && it.hasNext() && (!escaped || stack > 0 && prev == '\'')) {
if (prev == '\'') {
stack = 0;
escaped = false;
}
int number = extractIndex(it);
if (number >= 0) {
result.add(number);
}
} else if (escaped && c == '{') {
stack += 1;
} else if (escaped && c == '}') {
stack = Math.max(0, stack - 1);
if (stack == 0) {
escaped = false;
}
} else if (escaped && stack == 0) {
escaped = false;
}
prev = c;
}
return unmodifiableSet(result);
}

private static int extractIndex(PrimitiveIterator.OfInt iterator) {
int c = iterator.next();
while (Character.isWhitespace(c)) {
c = iterator.next();
}
int number = 0;
while (Character.isDigit(c)) {
number = number * 10 + Character.digit(c, 10);
c = iterator.next();
}
while (Character.isWhitespace(c)) {
c = iterator.next();
}
return c == '}' || c == ',' ? number : -1;
}
}
52 changes: 35 additions & 17 deletions src/main/java/com/coditory/quark/i18n/ArgumentResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,32 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.coditory.quark.i18n.Preconditions.expectNonNull;
import static java.util.stream.Collectors.toMap;

final class ArgumentResolver {
private final Map<Class<?>, I18nArgTransformer<?>> transformers;

static ArgumentResolver of(List<I18nArgTransformer<?>> transformers) {
expectNonNull(transformers, "transformers");
Map<Class<?>, I18nArgTransformer<?>> map = transformers.stream().collect(toMap(I18nArgTransformer::getArgType, it -> it));
return new ArgumentResolver(map);
}

private ArgumentResolver(Map<Class<?>, I18nArgTransformer<?>> transformers) {
expectNonNull(transformers, "transformers");
this.transformers = Map.copyOf(transformers);
}

Object[] resolveArguments(Object[] args) {
Object[] resolveArguments(Object[] args, Set<Integer> usedIndexes) {
expectNonNull(args, "args");
expectNonNull(usedIndexes, "usedIndexes");
boolean transformable = false;
for (Object arg : args) {
if (arg != null && transformers.containsKey(arg.getClass())) {
for (int i = 0; i < args.length; ++i) {
Object arg = args[i];
if (arg != null && usedIndexes.contains(i) && transformers.containsKey(arg.getClass())) {
transformable = true;
break;
}
Expand All @@ -31,39 +38,50 @@ Object[] resolveArguments(Object[] args) {
}
Object[] result = new Object[args.length];
for (int i = 0; i < args.length; ++i) {
result[i] = transformArgument(args[i]);
Object arg = args[i];
if (arg != null && usedIndexes.contains(i)) {
result[i] = transformArgument(args[i], i);
}
}
return result;
}

Map<String, Object> resolveArguments(Map<String, Object> args) {
boolean transformable = args.values().stream()
.anyMatch(a -> a != null && transformers.containsKey(a.getClass()));
if (!transformable) {
return args;
}
Map<String, Object> resolveArguments(Map<String, Object> args, Set<String> usedArgumentNames) {
expectNonNull(args, "args");
expectNonNull(usedArgumentNames, "usedArgumentNames");
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Object> entry : args.entrySet()) {
String key = entry.getKey();
Object value = transformArgument(entry.getValue());
Object value = entry.getValue();
if (value != null && usedArgumentNames.contains(key)) {
value = transformArgument(value, key);
}
result.put(key, value);
}
return result;
}

private Object transformArgument(Object argument, Object nameOrIndex) {
try {
return transformArgumentWithLimit(argument, 0);
} catch (Throwable e) {
throw new IllegalArgumentException("Could not transform argument: " + nameOrIndex + "=" + argument, e);
}
}

@SuppressWarnings("unchecked")
private Object transformArgument(Object argument) {
private Object transformArgumentWithLimit(Object argument, int count) {
if (count > 10) {
throw new IllegalArgumentException("Too many argument transformations");
}
if (argument == null) {
return null;
}
I18nArgTransformer<Object> transformer = (I18nArgTransformer<Object>) transformers.get(argument.getClass());
if (transformer == null) {
return argument;
}
try {
return transformer.transform(argument);
} catch (Throwable e) {
throw new IllegalArgumentException("Could not transform argument: " + argument, e);
}
Object transformed = transformer.transform(argument);
return transformArgumentWithLimit(transformed, count + 1);
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/coditory/quark/i18n/Currencies.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ private Currencies() {

@NotNull
public static String formatByCurrency(@NotNull BigDecimal amount, @NotNull Currency currency) {
expectNonNull(amount, "amount");
expectNonNull(currency, "currency");
Locale currencyLocale = LOCALE_BY_CURRENCY.get(currency);
if (currencyLocale == null) {
throw new IllegalArgumentException("Unrecognized currency: " + currency);
Expand All @@ -78,6 +80,7 @@ public static String formatByCurrency(@NotNull BigDecimal amount, @NotNull Curre

@NotNull
public static String formatByCurrency(long amount, @NotNull Currency currency) {
expectNonNull(currency, "currency");
Locale currencyLocale = LOCALE_BY_CURRENCY.get(currency);
if (currencyLocale == null) {
throw new IllegalArgumentException("Missing locale for currency: " + currency);
Expand All @@ -87,6 +90,7 @@ public static String formatByCurrency(long amount, @NotNull Currency currency) {

@NotNull
public static String formatByCurrency(double amount, @NotNull Currency currency) {
expectNonNull(currency, "currency");
Locale currencyLocale = LOCALE_BY_CURRENCY.get(currency);
if (currencyLocale == null) {
throw new IllegalArgumentException("Missing locale for currency: " + currency);
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/com/coditory/quark/i18n/I18nArgTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import java.util.function.Function;

import static java.util.Objects.requireNonNull;
import static com.coditory.quark.i18n.Preconditions.expectNonNull;

interface I18nArgTransformer<T> {
static <T> I18nArgTransformer<T> of(Class<T> type, Function<T, Object> transform) {
Expand All @@ -17,13 +17,13 @@ static <T> I18nArgTransformer<T> of(Class<T> type, Function<T, Object> transform
Object transform(@NotNull T value);
}

class SimpleI18nArgTransformer<T> implements I18nArgTransformer<T> {
final class SimpleI18nArgTransformer<T> implements I18nArgTransformer<T> {
private final Class<T> type;
private final Function<T, Object> transform;

public SimpleI18nArgTransformer(Class<T> type, Function<T, Object> transform) {
this.type = requireNonNull(type);
this.transform = requireNonNull(transform);
this.type = expectNonNull(type, "type");
this.transform = expectNonNull(transform, "transform");
}

@Override
Expand All @@ -32,7 +32,9 @@ public Class<T> getArgType() {
}

@Override
public @NotNull Object transform(@NotNull T value) {
@NotNull
public Object transform(@NotNull T value) {
expectNonNull(value, "value");
return transform.apply(value);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/coditory/quark/i18n/I18nKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public String pathValue() {
}

public String toShortString() {
return "" + locale + ':' + path;
String localeString = locale.toString().replaceAll("_", "-");
return localeString + ':' + path;
}

@Override
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/coditory/quark/i18n/I18nKeyGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,38 @@
import java.util.List;
import java.util.Locale;

import static com.coditory.quark.i18n.Preconditions.expectNonNull;

final class I18nKeyGenerator {
private final List<Locale> defaultLocales;
private final List<I18nPath> globalPrefixes;
private final LocaleResolver localeResolver;

public I18nKeyGenerator(Locale defaultLocale, List<I18nPath> globalPrefixes, LocaleResolver localeResolver) {
expectNonNull(localeResolver, "localeResolver");
expectNonNull(globalPrefixes, "globalPrefixes");
this.defaultLocales = defaultLocale != null
? localeResolver.getLocaleHierarchy(defaultLocale)
: List.of();
this.globalPrefixes = globalPrefixes;
this.globalPrefixes = List.copyOf(globalPrefixes);
this.localeResolver = localeResolver;
}

List<I18nKey> keys(I18nKey key) {
expectNonNull(key, "key");
return keys(key, List.of());
}

List<I18nKey> keys(I18nKey key, I18nPath prefix) {
expectNonNull(key, "key");
return prefix == null || prefix.isRoot()
? keys(key)
: keys(key, List.of(prefix));
}

List<I18nKey> keys(I18nKey key, List<I18nPath> prefixes) {
expectNonNull(key, "key");
expectNonNull(prefixes, "prefixes");
List<Locale> locales = localeResolver.getLocaleHierarchy(key.locale());
I18nPath path = key.path();
List<I18nKey> keys = new ArrayList<>(6 * (1 + prefixes.size() + globalPrefixes.size()));
Expand Down
Loading

0 comments on commit 6460110

Please sign in to comment.