org.zaproxy.zap.utils.I18N Maven / Gradle / Ivy
Show all versions of zap Show documentation
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2012 The ZAP Development Team
*
* 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.zaproxy.zap.utils;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
public class I18N {
/*
* Utility class for handling multiple message bundles.
* This allows extensions to manage their own i18n files.
*/
private ResourceBundle stdMessages = null;
/**
* Fallback resource bundle for when a resource is missing in the file system resource bundle.
*
* It uses the resource bundled in the JAR.
*
* @see #loadBundledResourceBundle(ZapResourceBundleControl)
*/
private ResourceBundle fallbackStdMessages;
private Locale locale = null;
private Map addonMessages = new HashMap<>();
/**
* A set of missing keys, to {@link #handleMissingResourceException(MissingResourceException)
* log an error} just once.
*
* The keys are removed when the corresponding bundle is removed (e.g. same prefix) or if a
* new bundle already defines it (e.g. contained in a resource bundle of an updated add-on),
* still, it might be possible that some keys are kept in memory until ZAP is shutdown.
*/
private Set missingKeys = Collections.synchronizedSet(new HashSet<>());
/**
* The class loader for file system Messages resource bundle.
*
* @see #createLangClassLoader()
* @see #loadFsResourceBundle(ZapResourceBundleControl)
*/
private URLClassLoader langClassLoader;
private static final Logger logger = Logger.getLogger(I18N.class);
public I18N(Locale locale) {
langClassLoader = createLangClassLoader();
setLocale(locale);
}
private static URLClassLoader createLangClassLoader() {
Path langDir = Paths.get(Constant.getZapInstall(), Constant.LANG_DIR);
if (Files.exists(langDir)) {
try {
return new URLClassLoader(new URL[] {langDir.toUri().toURL()});
} catch (MalformedURLException e) {
logger.warn("Failed to convert path " + langDir, e);
}
}
return null;
}
/**
* Gets the core resource bundle.
*
* @return the core resource bundle, never {@code null}.
* @since 2.7.0
*/
public ResourceBundle getCoreResourceBundle() {
return this.stdMessages;
}
public void addMessageBundle(String prefix, ResourceBundle bundle) {
logger.debug("Adding message bundle with prefix: " + prefix);
if (addonMessages.containsKey(prefix)) {
logger.error("Adding message bundle with duplicate prefix: " + prefix);
}
addonMessages.put(prefix, bundle);
}
public void removeMessageBundle(String prefix) {
missingKeys.removeIf(k -> k.startsWith(prefix));
logger.debug("Removing message bundle with prefix: " + prefix);
if (addonMessages.containsKey(prefix)) {
addonMessages.remove(prefix);
} else {
logger.debug("Message bundle not found, prefix: " + prefix);
}
}
public ResourceBundle getMessageBundle(String prefix) {
return this.addonMessages.get(prefix);
}
/**
* Gets the message with the given key.
*
* The message will be obtained either from the core {@link ResourceBundle} or a {@code
* ResourceBundle} of an add-on (depending on the prefix of the key).
*
*
Note: Since 2.8.0 this method no longer throws a {@code
* MissingResourceException} if the key does not exist, instead it logs an error and returns the
* key itself. This avoids breaking ZAP when a resource message is accidentally missing. Use
* {@link #containsKey(String)} instead to know if a message exists or not.
*
* @param key the key.
* @return the message.
* @see #getString(String, Object...)
* @see #getMessageBundle(String)
*/
public String getString(String key) {
try {
return this.getStringImpl(key);
} catch (MissingResourceException e) {
return handleMissingResourceException(e);
}
}
private String handleMissingResourceException(MissingResourceException e) {
logger.error("Failed to load a message:", e);
String key = e.getKey();
missingKeys.add(key);
return missingKeyReplacement(key);
}
private static String missingKeyReplacement(String key) {
return '!' + key + '!';
}
private String getStringImpl(String key) {
if (missingKeys.contains(key)) {
if (!containsKey(key)) {
return missingKeyReplacement(key);
}
missingKeys.remove(key);
}
if (key.indexOf(".") > 0) {
String prefix = key.substring(0, key.indexOf("."));
ResourceBundle bundle = this.addonMessages.get(prefix);
if (bundle != null && bundle.containsKey(key)) {
return bundle.getString(key);
}
}
try {
return this.stdMessages.getString(key);
} catch (MissingResourceException e) {
if (fallbackStdMessages != null) {
return fallbackStdMessages.getString(key);
}
throw e;
}
}
/**
* Gets the String with the given key surrounded by {@code
} tags.
*
* @param key the key of the string
* @return the string read wrapped in HTML and paragraph tags
*/
public String getHtmlWrappedString(String key) {
String values = getStringImpl(key);
if (values == null) return null;
return "
" + values + "
";
}
/**
* Returns the specified char from the language file. As these are typically used for mnemonics
* the 'null' char is returned if the key is not defined
*
* @param key the key of the char
* @return the char read, or null char if not found
*/
public char getChar(String key) {
try {
String str = this.getStringImpl(key);
if (str.length() > 0) {
return str.charAt(0);
}
} catch (Exception e) {
// Not defined
}
return '\u0000';
}
/**
* Sets the current locale to the given one and gets a resource bundle for new locale.
*
* The call to this method has no effect if the given locale is the same as the current one.
*
* @param locale the new locale
* @see ResourceBundle#getBundle(String, Locale)
*/
public void setLocale(Locale locale) {
if (locale.equals(this.locale)) {
return;
}
this.locale = locale;
ZapResourceBundleControl rbc = new ZapResourceBundleControl();
ResourceBundle fsRb = loadFsResourceBundle(rbc);
if (fsRb != null) {
this.stdMessages = fsRb;
logger.debug("Using file system Messages resource bundle.");
try {
this.fallbackStdMessages = loadBundledResourceBundle(rbc);
} catch (MissingResourceException e) {
logger.warn("Failed to find bundled Messages resource bundle.");
}
} else {
this.stdMessages = loadBundledResourceBundle(rbc);
logger.debug("Using bundled Messages resource bundle.");
}
}
private ResourceBundle loadFsResourceBundle(ZapResourceBundleControl rbc) {
if (langClassLoader == null) {
return null;
}
ResourceBundle.clearCache(langClassLoader);
try {
return loadResourceBundle(Constant.MESSAGES_PREFIX, langClassLoader, rbc);
} catch (MissingResourceException e) {
logger.debug("Failed to load file system Messages resource bundle.", e);
}
return null;
}
private ResourceBundle loadBundledResourceBundle(ZapResourceBundleControl rbc) {
return loadResourceBundle("org.zaproxy.zap.resources." + Constant.MESSAGES_PREFIX, rbc);
}
private ResourceBundle loadResourceBundle(String path, ZapResourceBundleControl rbc) {
return loadResourceBundle(path, getClass().getClassLoader(), rbc);
}
private ResourceBundle loadResourceBundle(
String path, ClassLoader classLoader, ZapResourceBundleControl rbc) {
return ResourceBundle.getBundle(path, locale, classLoader, rbc);
}
public Locale getLocal() {
return this.locale;
}
public boolean containsKey(String key) {
if (key.indexOf(".") > 0) {
String prefix = key.substring(0, key.indexOf("."));
ResourceBundle bundle = this.addonMessages.get(prefix);
if (bundle != null && bundle.containsKey(key)) {
return true;
}
}
return this.stdMessages.containsKey(key);
}
/**
* Gets the message with the given key, formatted with the given parameters.
*
*
The message will be obtained either from the core {@link ResourceBundle} or a {@code
* ResourceBundle} of an add-on (depending on the prefix of the key) and then {@link
* MessageFormat#format(String, Object...) formatted}.
*
*
Note: This method does not throw a {@code MissingResourceException} if
* the key does not exist, instead it logs an error and returns the key itself. This avoids
* breaking ZAP when a resource message is accidentally missing. Use {@link
* #containsKey(String)} instead to know if a message exists or not.
*
* @param key the key.
* @param params the parameters to format the message.
* @return the message formatted.
* @see #getString(String)
*/
public String getString(String key, Object... params) {
try {
return MessageFormat.format(this.getStringImpl(key), params);
} catch (MissingResourceException e) {
return handleMissingResourceException(e);
}
}
}