Skip to content

Commit

Permalink
Merge pull request #23 from jamezp/issue22
Browse files Browse the repository at this point in the history
[22] Use a MethodHandles.Lookup to lookup the interface implementatio…
  • Loading branch information
jamezp authored Dec 13, 2024
2 parents 747ba4a + fda679f commit 65199fb
Show file tree
Hide file tree
Showing 2 changed files with 257 additions and 41 deletions.
177 changes: 177 additions & 0 deletions src/main/java/org/wildfly/core/launcher/logger/LoggingLocale.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2023 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.wildfly.core.launcher.logger;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Locale;

/**
* A simple utility to resolve the default locale to use for internationalized message bundles.
*
* @author <a href="mailto:[email protected]">James R. Perkins</a>
*/
@SuppressWarnings("SameParameterValue")
class LoggingLocale {

private static final Locale LOCALE = getDefaultLocale();

/**
* Attempts to create a {@link Locale} based on the {@code org.jboss.logging.locale} system property. If the value
* is not defined the {@linkplain Locale#getDefault() default locale} will be used.
* <p>
* The value should be in the <a href="https://tools.ietf.org/html/bcp47">BCP 47</a> format.
* </p>
* <p>
* <strong>Note:</strong> Currently this uses a custom parser to attempt to parse the BCP 47 format. This will be
* changed to use the {@code Locale.forLanguageTag()} once a move to JDK 7. Currently only the language, region and
* variant are used to construct the locale.
* </p>
*
* @return the locale created or the default locale
*/
static Locale getLocale() {
return LOCALE;
}

private static Locale getDefaultLocale() {
final String bcp47Tag;
if (System.getSecurityManager() == null) {
bcp47Tag = System.getProperty("org.jboss.logging.locale", "");
} else {
bcp47Tag = AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty("org.jboss.logging.locale", ""));
}
if (bcp47Tag.trim().isEmpty()) {
return Locale.getDefault();
}
// When we upgrade to Java 7 we can use the Locale.forLanguageTag(locale) which will reliably parse the
// the value. For now we have to attempt to parse it the best we can.
return forLanguageTag(bcp47Tag);
}

private static Locale forLanguageTag(final String locale) {
// First check known locales
if ("en-CA".equalsIgnoreCase(locale)) {
return Locale.CANADA;
} else if ("fr-CA".equalsIgnoreCase(locale)) {
return Locale.CANADA_FRENCH;
} else if ("zh".equalsIgnoreCase(locale)) {
return Locale.CHINESE;
} else if ("en".equalsIgnoreCase(locale)) {
return Locale.ENGLISH;
} else if ("fr-FR".equalsIgnoreCase(locale)) {
return Locale.FRANCE;
} else if ("fr".equalsIgnoreCase(locale)) {
return Locale.FRENCH;
} else if ("de".equalsIgnoreCase(locale)) {
return Locale.GERMAN;
} else if ("de-DE".equalsIgnoreCase(locale)) {
return Locale.GERMANY;
} else if ("it".equalsIgnoreCase(locale)) {
return Locale.ITALIAN;
} else if ("it-IT".equalsIgnoreCase(locale)) {
return Locale.ITALY;
} else if ("ja-JP".equalsIgnoreCase(locale)) {
return Locale.JAPAN;
} else if ("ja".equalsIgnoreCase(locale)) {
return Locale.JAPANESE;
} else if ("ko-KR".equalsIgnoreCase(locale)) {
return Locale.KOREA;
} else if ("ko".equalsIgnoreCase(locale)) {
return Locale.KOREAN;
} else if ("zh-CN".equalsIgnoreCase(locale)) {
return Locale.SIMPLIFIED_CHINESE;
} else if ("zh-TW".equalsIgnoreCase(locale)) {
return Locale.TRADITIONAL_CHINESE;
} else if ("en-UK".equalsIgnoreCase(locale)) {
return Locale.UK;
} else if ("en-US".equalsIgnoreCase(locale)) {
return Locale.US;
}

// Split the string into parts and attempt
final String[] parts = locale.split("-");
final int len = parts.length;
int index = 0;
int count = 0;
final String language = parts[index++];
String region = ""; // country
String variant = "";
// The next 3 sections may be extended languages, we're just going to ignore them
while (index < len) {
if (count++ == 2 || !isAlpha(parts[index], 3, 3)) {
break;
}
index++;
}
// Check for a script, we'll skip it however a script is not supported until Java 7
if (index != len && isAlpha(parts[index], 4, 4)) {
index++;
}
// Next should be the region, 3 digit is allowed but may not work with Java 6
if (index != len && (isAlpha(parts[index], 2, 2) || isNumeric(parts[index], 3, 3))) {
region = parts[index++];
}
// Next should be the variant and we will just use the first one found, all other parts will be ignored
if (index != len && (isAlphaOrNumeric(parts[index], 5, 8))) {
variant = parts[index];
}
return new Locale(language, region, variant);
}

private static boolean isAlpha(final String value, final int minLen, final int maxLen) {
final int len = value.length();
if (len < minLen || len > maxLen) {
return false;
}
for (int i = 0; i < len; i++) {
if (!Character.isLetter(value.charAt(i))) {
return false;
}
}
return true;
}

private static boolean isNumeric(final String value, final int minLen, final int maxLen) {
final int len = value.length();
if (len < minLen || len > maxLen) {
return false;
}
for (int i = 0; i < len; i++) {
if (!Character.isDigit(value.charAt(i))) {
return false;
}
}
return true;
}

private static boolean isAlphaOrNumeric(final String value, final int minLen, final int maxLen) {

final int len = value.length();
if (len < minLen || len > maxLen) {
return false;
}
for (int i = 0; i < len; i++) {
if (!Character.isLetterOrDigit(value.charAt(i))) {
return false;
}
}
return true;
}
}
121 changes: 80 additions & 41 deletions src/main/java/org/wildfly/core/launcher/logger/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import static java.security.AccessController.doPrivileged;

import java.lang.reflect.Field;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.security.PrivilegedAction;
import java.util.Locale;

Expand All @@ -30,52 +32,28 @@ private Messages() {
* @return the bundle
*/
public static <T> T getBundle(final Class<T> type) {
return doPrivileged(new PrivilegedAction<T>() {
public T run() {
final Locale locale = Locale.getDefault();
final String lang = locale.getLanguage();
final String country = locale.getCountry();
final String variant = locale.getVariant();

Class<? extends T> bundleClass = null;
if (variant != null && !variant.isEmpty()) try {
bundleClass = Class.forName(join(type.getName(), "$bundle", lang, country, variant), true, type.getClassLoader()).asSubclass(type);
} catch (ClassNotFoundException e) {
// ignore
}
if (bundleClass == null && country != null && !country.isEmpty()) try {
bundleClass = Class.forName(join(type.getName(), "$bundle", lang, country, null), true, type.getClassLoader()).asSubclass(type);
} catch (ClassNotFoundException e) {
// ignore
}
if (bundleClass == null && lang != null && !lang.isEmpty()) try {
bundleClass = Class.forName(join(type.getName(), "$bundle", lang, null, null), true, type.getClassLoader()).asSubclass(type);
} catch (ClassNotFoundException e) {
// ignore
}
if (bundleClass == null) try {
bundleClass = Class.forName(join(type.getName(), "$bundle", null, null, null), true, type.getClassLoader()).asSubclass(type);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Invalid bundle " + type + " (implementation not found)");
}
final Field field;
try {
field = bundleClass.getField("INSTANCE");
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Bundle implementation " + bundleClass + " has no instance field");
}
if (System.getSecurityManager() == null) {
try {
final Lookup lookup = MethodHandles.privateLookupIn(type, MethodHandles.lookup());
return doGetBundle(lookup, type);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("This library does not have private access to " + type);
}
} else {
return doPrivileged((PrivilegedAction<T>) () -> {
try {
return type.cast(field.get(null));
final Lookup lookup = MethodHandles.privateLookupIn(type, MethodHandles.lookup());
return doGetBundle(lookup, type);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Bundle implementation " + bundleClass + " could not be instantiated", e);
throw new IllegalArgumentException("This library does not have private access to " + type);
}
}
});
});
}
}

private static String join(final String interfaceName, final String type, final String lang, final String country, final String variant) {
private static String join(final String interfaceName, final String lang, final String country, final String variant) {
final StringBuilder build = new StringBuilder();
build.append(interfaceName).append('_').append(type);
build.append(interfaceName).append('_').append("$bundle");
if (lang != null && !lang.isEmpty()) {
build.append('_');
build.append(lang);
Expand All @@ -90,4 +68,65 @@ private static String join(final String interfaceName, final String type, final
}
return build.toString();
}

private static <T> T doGetBundle(final MethodHandles.Lookup lookup, final Class<T> type) {
final Locale locale = LoggingLocale.getLocale();
final String language = locale.getLanguage();
final String country = locale.getCountry();
final String variant = locale.getVariant();

Class<? extends T> bundleClass = null;
if (!variant.isEmpty()) {
try {
bundleClass = lookup.findClass(join(type.getName(), language, country, variant))
.asSubclass(type);
} catch (ClassNotFoundException e) {
// ignore
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("The given lookup does not have access to the implementation class");
}
}
if (bundleClass == null && !country.isEmpty()) {
try {
bundleClass = lookup.findClass(join(type.getName(), language, country, null))
.asSubclass(type);
} catch (ClassNotFoundException e) {
// ignore
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("The given lookup does not have access to the implementation class");
}
}
if (bundleClass == null && !language.isEmpty()) {
try {
bundleClass = lookup.findClass(join(type.getName(), language, null, null)).asSubclass(type);
} catch (ClassNotFoundException e) {
// ignore
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("The given lookup does not have access to the implementation class");
}
}
if (bundleClass == null) {
try {
bundleClass = lookup.findClass(join(type.getName(), null, null, null)).asSubclass(type);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Invalid bundle " + type + " (implementation not found)");
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("The given lookup does not have access to the implementation class");
}
}
final MethodHandle getter;
try {
getter = lookup.findStaticGetter(bundleClass, "INSTANCE", bundleClass);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Bundle implementation " + bundleClass + " has no instance field");
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"The given lookup does not have access to the implementation class instance field");
}
try {
return type.cast(getter.invoke());
} catch (Throwable e) {
throw new IllegalArgumentException("Bundle implementation " + bundleClass + " could not be instantiated", e);
}
}
}

0 comments on commit 65199fb

Please sign in to comment.