strolch/utils/src/main/java/li/strolch/utils/I18nMessage.java

483 lines
15 KiB
Java

package li.strolch.utils;
import static java.util.Collections.emptySet;
import static li.strolch.utils.collections.SynchronizedCollections.synchronizedMapOfSets;
import static li.strolch.utils.helper.ExceptionHelper.formatException;
import static li.strolch.utils.helper.ExceptionHelper.getExceptionMessageWithCauses;
import static li.strolch.utils.helper.StringHelper.EMPTY;
import static li.strolch.utils.helper.StringHelper.isEmpty;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.CodeSource;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import li.strolch.utils.collections.MapOfMaps;
import li.strolch.utils.collections.MapOfSets;
import li.strolch.utils.collections.TypedTuple;
import li.strolch.utils.dbc.DBC;
import li.strolch.utils.helper.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class I18nMessage {
private static final Logger logger = LoggerFactory.getLogger(I18nMessage.class);
private static MapOfMaps<String, Locale, ResourceBundle> bundleMap;
private static final MapOfSets<String, String> missingKeysMap = synchronizedMapOfSets(new MapOfSets<>());
private final String bundleName;
private final String key;
private final Properties values;
private final ResourceBundle bundle;
private String message;
protected Throwable exception;
protected String stackTrace;
public I18nMessage(ResourceBundle bundle, String key) {
DBC.INTERIM.assertNotNull("bundle may not be null!", bundle);
DBC.INTERIM.assertNotEmpty("key must be set!", key);
this.key = key.intern();
this.values = new Properties();
this.bundle = bundle;
this.bundleName = bundle.getBaseBundleName().intern();
}
public I18nMessage(String bundle, String key, Properties values, String message) {
DBC.INTERIM.assertNotNull("bundle must not be empty!", bundle);
DBC.INTERIM.assertNotEmpty("key must be set!", key);
DBC.INTERIM.assertNotEmpty("message must be set!", message);
this.key = key.intern();
this.values = values == null ? new Properties() : values;
this.message = message;
this.bundle = findBundle(bundle);
this.bundleName = this.bundle == null ? bundle : this.bundle.getBaseBundleName();
}
public I18nMessage(I18nMessage other) {
this.key = other.key;
this.values = new Properties(other.values);
this.bundle = other.bundle;
this.bundleName = other.bundleName;
this.message = other.message;
this.exception = other.exception;
this.stackTrace = other.stackTrace;
}
public String getKey() {
return this.key;
}
public String getBundle() {
if (this.bundle == null)
return "";
return this.bundle.getBaseBundleName();
}
public Properties getValues() {
return this.values;
}
public Object getValue(String key) {
return this.values.getOrDefault(key, null);
}
private ResourceBundle getBundle(Locale locale) {
if (this.bundle == null)
return null;
if (this.bundle.getLocale() == locale)
return this.bundle;
String baseName = this.bundle.getBaseBundleName();
try {
ClassLoader classLoader = this.bundle.getClass().getClassLoader();
if (classLoader == null)
return ResourceBundle.getBundle(baseName, locale);
return ResourceBundle.getBundle(baseName, locale, classLoader);
} catch (MissingResourceException e) {
if (!missingKeysMap.containsSet(baseName + "_" + locale.toLanguageTag())) {
logger.error("Failed to find resource bundle " + baseName + " " + locale.toLanguageTag()
+ ", returning current bundle " + this.bundle.getLocale().toLanguageTag());
missingKeysMap.addSet(baseName + "_" + locale.toLanguageTag(), emptySet());
}
return this.bundle;
}
}
public String getMessage(ResourceBundle bundle) {
DBC.INTERIM.assertNotNull("bundle may not be null!", bundle);
return formatMessage(bundle);
}
public String getMessage(Locale locale) {
ResourceBundle bundle = getBundle(locale);
if (bundle == null) {
if (isEmpty(this.bundleName))
return getMessage();
if (!missingKeysMap.containsSet(this.bundleName + "_" + locale.toLanguageTag())) {
logger.warn("No bundle found for " + this.bundleName + " " + locale + ". Available are: ");
getBundleMap().forEach((s, map) -> {
logger.info(" " + s);
map.forEach((l, resourceBundle) -> logger.info(" " + l + ": " + map.keySet()));
});
missingKeysMap.addSet(this.bundleName + "_" + locale.toLanguageTag(), emptySet());
}
return getMessage();
}
return formatMessage(bundle);
}
public String getMessage() {
return formatMessage();
}
public I18nMessage value(String key, Object value) {
DBC.INTERIM.assertNotEmpty("key must be set!", key);
this.values.setProperty(key, value == null ? "(null)" : value.toString());
return this;
}
public I18nMessage value(String key, Throwable t) {
this.exception = t;
this.stackTrace = formatException(t);
value(key, getExceptionMessageWithCauses(t));
return this;
}
public I18nMessage withException(Throwable t) {
this.exception = t;
this.stackTrace = formatException(t);
return this;
}
public boolean hasException() {
return this.exception != null;
}
public Throwable getException() {
return exception;
}
public String getStackTrace() {
return this.stackTrace;
}
public String formatMessage() {
if (this.message != null)
return this.message;
if (this.bundle == null) {
this.message = this.key;
return this.message;
}
this.message = formatMessage(this.bundle);
return this.message;
}
public String formatMessage(ResourceBundle bundle) {
try {
String string = bundle.getString(this.key);
return StringHelper.replacePropertiesIn(this.values, EMPTY, string);
} catch (MissingResourceException e) {
String baseName = bundle.getBaseBundleName();
String languageTag = bundle.getLocale().toLanguageTag();
String bundleKey = baseName + "_" + languageTag;
if (!missingKeysMap.containsElement(bundleKey, this.key)) {
logger.error("Key " + this.key + " is missing in bundle " + baseName + " for locale " + languageTag);
missingKeysMap.addElement(bundleKey, this.key);
}
return this.key;
}
}
public <T> T accept(I18nMessageVisitor<T> visitor) {
return visitor.visit(this);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.key == null) ? 0 : this.key.hashCode());
result = prime * result + ((this.values == null) ? 0 : this.values.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
I18nMessage other = (I18nMessage) obj;
if (this.key == null) {
if (other.key != null)
return false;
} else if (!this.key.equals(other.key))
return false;
if (this.values == null) {
if (other.values != null)
return false;
} else if (!this.values.equals(other.values))
return false;
return true;
}
@Override
public String toString() {
return getMessage(Locale.getDefault());
}
private ResourceBundle findBundle(String baseName) {
if (baseName.isEmpty())
return null;
Map<Locale, ResourceBundle> bundlesByLocale = getBundleMap().getMap(baseName);
if (bundlesByLocale == null || bundlesByLocale.isEmpty())
return null;
ResourceBundle bundle = bundlesByLocale.get(Locale.getDefault());
if (bundle != null)
return bundle;
return bundlesByLocale.values().iterator().next();
}
private static MapOfMaps<String, Locale, ResourceBundle> getBundleMap() {
if (bundleMap == null) {
synchronized (logger) {
bundleMap = findAllBundles();
}
}
return bundleMap;
}
private static MapOfMaps<String, Locale, ResourceBundle> findAllBundles() {
try {
CodeSource src = I18nMessage.class.getProtectionDomain().getCodeSource();
if (src == null) {
logger.error(
"Failed to find CodeSource for ProtectionDomain " + I18nMessage.class.getProtectionDomain());
return new MapOfMaps<>();
}
File jarLocationF = new File(src.getLocation().toURI());
if (!(jarLocationF.exists() && jarLocationF.getParentFile().isDirectory())) {
logger.info("Found JAR repository at " + jarLocationF.getParentFile());
return new MapOfMaps<>();
}
MapOfMaps<String, Locale, ResourceBundle> bundleMap = new MapOfMaps<>();
File jarD = jarLocationF.getParentFile();
File[] jarFiles = jarD.listFiles((dir, name) -> name.endsWith(".jar"));
if (jarFiles == null)
return new MapOfMaps<>();
for (File file : jarFiles) {
if (shouldIgnoreFile(file))
continue;
try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry je = entries.nextElement();
String entryName = je.getName();
if (entryName.startsWith("META-INF") //
|| entryName.equals("ENV.properties") //
|| entryName.equals("agentVersion.properties") //
|| entryName.equals("appVersion.properties") //
|| entryName.equals("componentVersion.properties") //
|| entryName.equals("strolch_db_version.properties"))
continue;
if (!entryName.endsWith(".properties"))
continue;
TypedTuple<String, Locale> tuple = parsePropertyName(entryName);
if (tuple == null)
continue;
String baseName = tuple.getFirst();
Locale locale = tuple.getSecond();
ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale,
new CustomControl(jarFile.getInputStream(je)));
bundleMap.addElement(bundle.getBaseBundleName(), bundle.getLocale(), bundle);
String propertyName = entryName.replace('/', '.');
logger.info(
" Loaded bundle " + bundle.getBaseBundleName() + " " + bundle.getLocale() + " from "
+ propertyName + " from JAR " + file.getName());
}
}
}
File classesD = new File(jarD.getParentFile(), "classes");
if (classesD.isDirectory()) {
File[] propertyFiles = classesD.listFiles(
(dir, name) -> name.endsWith(".properties") && !(name.equals("appVersion.properties")
|| name.equals("ENV.properties")));
if (propertyFiles != null) {
for (File propertyFile : propertyFiles) {
logger.info(" Found property file " + propertyFile.getName() + " in classes "
+ classesD.getAbsolutePath());
TypedTuple<String, Locale> tuple = parsePropertyName(propertyFile.getName());
if (tuple == null)
continue;
String baseName = tuple.getFirst();
Locale locale = tuple.getSecond();
ResourceBundle bundle;
try (FileInputStream in = new FileInputStream(propertyFile)) {
bundle = ResourceBundle.getBundle(baseName, locale, new CustomControl(in));
}
bundleMap.addElement(bundle.getBaseBundleName(), bundle.getLocale(), bundle);
logger.info(" Loaded bundle " + bundle.getBaseBundleName() + " " + bundle.getLocale()
+ " from file " + propertyFile.getName());
}
}
}
logger.info("Done.");
return bundleMap;
} catch (Exception e) {
logger.error("Failed to find all property files!", e);
return new MapOfMaps<>();
}
}
private static TypedTuple<String, Locale> parsePropertyName(String entryName) {
String propertyName = entryName.replace('/', '.');
String bundleName = propertyName.substring(0, propertyName.lastIndexOf("."));
String baseName;
Locale locale;
int i = bundleName.indexOf('_');
if (i > 0) {
baseName = bundleName.substring(0, i);
String localeS = bundleName.substring(i + 1);
String[] parts = localeS.split("_");
if (parts.length == 2) {
String language = parts[0];
String country = parts[1];
int languageI = Arrays.binarySearch(Locale.getISOLanguages(), language);
int countryI = Arrays.binarySearch(Locale.getISOCountries(), country);
if (languageI >= 0 && countryI >= 0)
locale = new Locale(language, country);
else {
logger.warn("Ignoring bad bundle locale for " + entryName);
return null;
}
} else {
int languageI = Arrays.binarySearch(Locale.getISOLanguages(), localeS);
if (languageI >= 0)
locale = new Locale(localeS);
else {
logger.warn("Ignoring bad bundle locale for " + entryName);
return null;
}
}
} else {
baseName = bundleName;
locale = Locale.getDefault();
}
return new TypedTuple<>(baseName, locale);
}
private static class CustomControl extends ResourceBundle.Control {
private final InputStream stream;
public CustomControl(InputStream stream) {
this.stream = stream;
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader,
boolean reload) throws IOException {
return new PropertyResourceBundle(this.stream);
}
}
private static boolean shouldIgnoreFile(File file) {
return file.getName().contains("aopalliance") //
|| file.getName().contains("activation") //
|| file.getName().contains("antlr") //
|| file.getName().contains("assertj-core") //
|| file.getName().startsWith("com.sun") //
|| file.getName().startsWith("commonj.") //
|| file.getName().startsWith("commons-") //
|| file.getName().startsWith("jackson-") //
|| file.getName().startsWith("hapi-") //
|| file.getName().startsWith("jaxb-") //
|| file.getName().startsWith("org.hl7.") //
|| file.getName().startsWith("listenablefuture-") //
|| file.getName().startsWith("j2objc-annotations") //
|| file.getName().startsWith("failureaccess-") //
|| file.getName().startsWith("error_prone_") //
|| file.getName().startsWith("guava-") //
|| file.getName().startsWith("org.eclipse") //
|| file.getName().startsWith("javax") //
|| file.getName().startsWith("jaxws") //
|| file.getName().startsWith("jaxrs") //
|| file.getName().startsWith("jaxb") //
|| file.getName().contains("jsr305") //
|| file.getName().contains("c3p0") //
|| file.getName().contains("camel") //
|| file.getName().contains("checker-qual") //
|| file.getName().contains("cron") //
|| file.getName().contains("FastInfoset") //
|| file.getName().contains("gmbal") //
|| file.getName().contains("grizzly") //
|| file.getName().contains("gson") //
|| file.getName().contains("ha-api") //
|| file.getName().contains("HikariCP") //
|| file.getName().contains("hk2") //
|| file.getName().contains("icu4j") //
|| file.getName().contains("jakarta") //
|| file.getName().contains("javassist") //
|| file.getName().contains("jersey") //
|| file.getName().contains("joda-time") //
|| file.getName().contains("logback") //
|| file.getName().contains("management-api") //
|| file.getName().contains("mchange-commons-java") //
|| file.getName().contains("mimepull") //
|| file.getName().contains("org.abego.treelayout") //
|| file.getName().contains("osgi") //
|| file.getName().contains("pfl-basic") //
|| file.getName().contains("pfl-tf") //
|| file.getName().contains("policy-2.7.10") //
|| file.getName().contains("postgresql") //
|| file.getName().contains("quartz") //
|| file.getName().contains("saaj-impl") //
|| file.getName().contains("sax") //
|| file.getName().contains("slf4j") //
|| file.getName().contains("ST4") //
|| file.getName().contains("stax-ex") //
|| file.getName().contains("stax2-api") //
|| file.getName().contains("streambuffer") //
|| file.getName().contains("tyrus") //
|| file.getName().contains("validation-api") //
|| file.getName().contains("yasson");
}
}