All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hsqldb.lib.RefCapablePropertyResourceBundle Maven / Gradle / Ivy

There is a newer version: 2.7.4
Show newest version
/* Copyright (c) 2001-2021, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.lib;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/* $Id: RefCapablePropertyResourceBundle.java 6266 2021-01-25 16:08:06Z fredt $ */

/**
 * Just like PropertyResourceBundle, except keys mapped to nothing in the
 * properties file will load the final String value from a text file.
 *
 * The use case is where one wants to use a ResourceBundle for Strings,
 * but some of the Strings are long-- too long to maintain in a Java
 * .properties file.
 * By using this class, you can put each such long String in its own
 * separate file, yet all keys mapped to (non-empty) values in the
 * .properties file will behave just like regular PropertyResourceBundle
 * properties.
 * In this documentation, I call these values read in atomically from
 * other files referenced values, because the values are not directly
 * in the .properties file, but are "referenced" in the .properties file
 * by virtue of the empty value for the key.
 *
 * You use this class in the same way as you would traditionally use
 * ResourceBundle:
 * 
 *  import org.hsqldb.util..RefCapablePropertyResourceBundle;
 *  ...
 *      RefCapablePropertyResourceBundle bundle =
 *              RefCapablePropertyResourceBundle.getBundle("subdir.xyz");
 *      System.out.println("Value for '1' = (" + bundle.getString("1") + ')');
 * 
* * Just like PropertyResourceBundle, the .properties file and the * referenced files are read in from the classpath by a class loader, * according to the normal ResourceBundle rules. * To eliminate the need to prohibit the use of any strings in the .properties * values, and to enforce consistency, you must use the following rules * to when putting your referenced files into place. *

* REFERENCED FILE DIRECTORY is a directory named with the base name of the * properties file, and in the same parent directory. So, the referenced * file directory /a/b/c/greentea is used to hold all reference * files for properties files /a/b/c/greentea_en_us.properties, * /a/b/c/greentea_de.properties, * /a/b/c/greentea.properties, etc. * (BTW, according to ResourceBundle rules, this resource should be looked * up with name "a.b.c.greentea", not "/a/b/c..." or "a/b/c"). * REFERENCED FILES themselves all have the base name of the property key, * with locale appendages exactly as the referring properties files * has, plus the suffix .text. *

* So, if we have the following line in * /a/b/c/greentea_de.properties: *

 *     1: eins
 * 
* then you must have a reference text file * /a/b/c/greentea/1_de.properties: *

* In reference text files, * sequences of "\r", "\n" and "\r\n" are all translated to the line * delimiter for your platform (System property line.separator). * If one of those sequences exists at the very end of the file, it will be * eliminated (so, if you really want getString() to end with a line delimiter, * end your file with two of them). * (The file itself is never modified-- I'm talking about the value returned * by getString(String)). *

* To prevent throwing at runtime due to unset variables, use a wrapper class * like SqltoolRB (use SqltoolRB.java as a template). * To prevent throwing at runtime due to unset System Properties, or * insufficient parameters passed to getString(String, String[]), set the * behavior values appropriately. *

* Just like all Properties files, referenced files must use ISO-8859-1 * encoding, with unicode escapes for characters outside of ISO-8859-1 * character set. But, unlike Properties files, \ does not need to be * escaped for normal usage. *

* The getString() methods with more than one parameter substitute for * "positional" parameters of the form "%{1}". * The getExpandedString() methods substitute for System Property names * of the form "${1}". * In both cases, you can interpose :+ and a string between the variable * name and the closing }. This works just like the Bourne shell * ${x:+y} feature. If "x" is set, then "y" is returned, and "y" may * contain references to the original variable without the curly braces. * In this file, I refer to the y text as the "conditional string". * One example of each type: *

 *     Out val = (${condlSysProp:+Prop condlSysProp is set to $condlSysProp.})
 *     Out val = (%{2:+Pos Var #2 is set to %2.})
 * OUTPUT if neither are set:
 *     Out val = ()
 *     Out val = ()
 * OUTPUT if condlSysProp=alpha and condlPLvar=beta:
 *     Out val = (Prop condlSysProp is set to alpha.)
 *     Out val = (Pos Var #2 is set to beta.)
 * 
* This feature has the following limitations. *
    *
  • The conditional string may only contain the primary variable. *
  • Inner instances of the primary variable may not use curly braces, * and therefore the variable name must end at a word boundary. *
* The conditional string may span newlines, and it is often very useful * to do so. * * @see java.util.PropertyResourceBundle * @see java.util.ResourceBundle * @author Blaine Simpson (blaine dot simpson at admc dot com) */ public class RefCapablePropertyResourceBundle { private PropertyResourceBundle wrappedBundle; private String baseName; private String language, country, variant; static private Map allBundles = new HashMap(); public static final String LS = System.getProperty("line.separator"); private Pattern sysPropVarPattern = Pattern.compile( "(?s)\\Q${\\E([^}]+?)(?:\\Q:+\\E([^}]+))?\\Q}"); private Pattern posPattern = Pattern.compile( "(?s)\\Q%{\\E(\\d)(?:\\Q:+\\E([^}]+))?\\Q}"); private Class> loaderClass; public static final int THROW_BEHAVIOR = 0; public static final int EMPTYSTRING_BEHAVIOR = 1; public static final int NOOP_BEHAVIOR = 2; public Enumeration getKeys() { return wrappedBundle.getKeys(); } private RefCapablePropertyResourceBundle(String baseName, PropertyResourceBundle wrappedBundle, Class> loaderClass) { this.baseName = baseName; this.wrappedBundle = wrappedBundle; Locale locale = wrappedBundle.getLocale(); this.loaderClass = loaderClass; language = locale.getLanguage(); country = locale.getCountry(); variant = locale.getVariant(); if (language.length() < 1) language = null; if (country.length() < 1) country = null; if (variant.length() < 1) variant = null; } /** * Same as getString(), but expands System Variables specified in * property values like ${sysvarname}. */ public String getExpandedString(String key, int behavior) { String s = getString(key); Matcher matcher = sysPropVarPattern.matcher(s); int previousEnd = 0; StringBuilder sb = new StringBuilder(); String varName, varValue; String condlVal; // Conditional : value while (matcher.find()) { varName = matcher.group(1); condlVal = ((matcher.groupCount() > 1) ? matcher.group(2) : null); varValue = System.getProperty(varName); if (condlVal != null) { // Replace varValue (the value to be substituted), with // the post-:+ portion of the expression. varValue = ((varValue == null) ? "" : condlVal.replaceAll("\\Q$" + varName + "\\E\\b", Matcher.quoteReplacement(varValue))); } if (varValue == null) switch (behavior) { case THROW_BEHAVIOR: throw new RuntimeException( "No Sys Property set for variable '" + varName + "' in property value (" + s + ")."); case EMPTYSTRING_BEHAVIOR: varValue = ""; break; case NOOP_BEHAVIOR: break; default: throw new RuntimeException( "Undefined value for behavior: " + behavior); } sb.append(s.substring(previousEnd, matcher.start()) + ((varValue == null) ? matcher.group() : varValue)); previousEnd = matcher.end(); } return (previousEnd < 1) ? s : (sb.toString() + s.substring(previousEnd)); } /** * Replaces positional substitution patterns of the form %{\d} with * corresponding element of the given subs array. * Note that %{\d} numbers are 1-based, so we lok for subs[x-1]. */ public String posSubst(String s, String[] subs, int behavior) { Matcher matcher = posPattern.matcher(s); int previousEnd = 0; StringBuilder sb = new StringBuilder(); String varValue; int varIndex; String condlVal; // Conditional : value while (matcher.find()) { varIndex = Integer.parseInt(matcher.group(1)) - 1; condlVal = ((matcher.groupCount() > 1) ? matcher.group(2) : null); varValue = ((varIndex < subs.length) ? subs[varIndex] : null); if (condlVal != null) { // Replace varValue (the value to be substituted), with // the post-:+ portion of the expression. varValue = ((varValue == null) ? "" : condlVal.replaceAll("\\Q%" + (varIndex+1) + "\\E\\b", Matcher.quoteReplacement(varValue))); } // System.err.println("Behavior: " + behavior); if (varValue == null) switch (behavior) { case THROW_BEHAVIOR: throw new RuntimeException( Integer.toString(subs.length) + " positional values given, but property string " + "contains (" + matcher.group() + ")."); case EMPTYSTRING_BEHAVIOR: varValue = ""; break; case NOOP_BEHAVIOR: break; default: throw new RuntimeException( "Undefined value for behavior: " + behavior); } sb.append(s.substring(previousEnd, matcher.start()) + ((varValue == null) ? matcher.group() : varValue)); previousEnd = matcher.end(); } return (previousEnd < 1) ? s : (sb.toString() + s.substring(previousEnd)); } public String getExpandedString(String key, String[] subs, int missingPropertyBehavior, int missingPosValueBehavior) { return posSubst(getExpandedString(key, missingPropertyBehavior), subs, missingPosValueBehavior); } public String getString(String key, String[] subs, int behavior) { return posSubst(getString(key), subs, behavior); } /** * Just identifies this RefCapablePropertyResourceBundle instance. */ public String toString() { return baseName + " for " + language + " / " + country + " / " + variant; } /** * Returns value defined in this RefCapablePropertyResourceBundle's * .properties file, unless that value is empty. * If the value in the .properties file is empty, then this returns * the entire contents of the referenced text file. * * @see ResourceBundle#getString(String) */ public String getString(String key) { String value = wrappedBundle.getString(key); if (value.length() < 1) { value = getStringFromFile(key); // For conciseness and sanity, get rid of all \r's so that \n // will definitively be our line breaks. if (value.indexOf('\r') > -1) value = value.replaceAll("\\Q\r\n", "\n") .replaceAll("\\Q\r", "\n"); if (value.length() > 0 && value.charAt(value.length() - 1) == '\n') value = value.substring(0, value.length() - 1); } return RefCapablePropertyResourceBundle.toNativeLs(value); } /** * @param inString Input string with \n definitively indicating desired * position for line separators. * @return If platform's line-separator is \n, then just returns inString. * Otherwise returns a copy of inString, with all \n's * transformed to the platform's line separators. */ public static String toNativeLs(String inString) { return LS.equals("\n") ? inString : inString.replaceAll("\\Q\n", LS); } /** * Use like java.util.ResourceBundle.getBundle(String). * * ClassLoader is required for our getBundles()s, since it is impossible * to get the "caller's" ClassLoader without using JNI (i.e., with pure * Java). * * @see ResourceBundle#getBundle(String) */ public static RefCapablePropertyResourceBundle getBundle(String baseName, ClassLoader loader, Class> loaderClass) { return getRef(baseName, ResourceBundle.getBundle(baseName, Locale.getDefault(), loader), loaderClass); } /** * Use exactly like java.util.ResourceBundle.get(String, Locale, ClassLoader). * * @see ResourceBundle#getBundle(String, Locale, ClassLoader) */ public static RefCapablePropertyResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader, Class> loaderClass) { return getRef(baseName, ResourceBundle.getBundle( baseName, locale, loader), loaderClass); } /** * Return a ref to a new or existing RefCapablePropertyResourceBundle, * or throw a MissingResourceException. */ static private RefCapablePropertyResourceBundle getRef(String baseName, ResourceBundle rb, Class> loaderClass) { if (!(rb instanceof PropertyResourceBundle)) throw new MissingResourceException( "Found a Resource Bundle, but it is a " + rb.getClass().getName(), PropertyResourceBundle.class.getName(), null); if (allBundles.containsKey(rb)) return allBundles.get(rb); RefCapablePropertyResourceBundle newPRAFP = new RefCapablePropertyResourceBundle(baseName, (PropertyResourceBundle) rb, loaderClass); allBundles.put(rb, newPRAFP); return newPRAFP; } /** * Recursive. * For consistency we do not allow paths relative to anything other * than root. Even though we load via Class, baseNames are resolved * as if absolute, just like ClassLoader lookups. */ private InputStream getMostSpecificStream( String key, String l, String c, String v) { // Now that using a Class to look up resource for this special case, // absolute paths need to begin with slash. final String filePath = (baseName.matches("^[^./].*[./].+") ? "/" : "") + baseName.replace('.', '/') + '/' + key + ((l == null) ? "" : ("_" + l)) + ((c == null) ? "" : ("_" + c)) + ((v == null) ? "" : ("_" + v)) + ".text"; //System.err.println("Seeking " + filePath + " FOR " + baseName); InputStream is = (InputStream) AccessController.doPrivileged( new PrivilegedAction() { public InputStream run() { return loaderClass.getResourceAsStream(filePath); } }); // N.b. If were using Class.getRes... instead of ClassLoader.getRes... // we would need to prefix the path with "/". return (is == null && l != null) ? getMostSpecificStream(key, ((c == null) ? null : l), ((v == null) ? null : c), null) : is; } private String getStringFromFile(String key) { byte[] ba = null; int bytesread = 0; int retval; InputStream inputStream = getMostSpecificStream(key, language, country, variant); if (inputStream == null) throw new MissingResourceException( "Key '" + key + "' is present in .properties file with no value, yet " + "text file resource is missing", RefCapablePropertyResourceBundle.class.getName(), key); try { try { ba = new byte[inputStream.available()]; } catch (IOException ioe) { throw new MissingResourceException( "Failed to read in value for key '" + key + "': " + ioe, RefCapablePropertyResourceBundle.class.getName(), key); } catch (Throwable re) { throw new MissingResourceException( "Resource is too big to read in '" + key + "' value in one " + "gulp.\nPlease run the program with more RAM " + "(try Java -Xm* switches).: " + re, RefCapablePropertyResourceBundle.class.getName(), key); } try { while (bytesread < ba.length && (retval = inputStream.read( ba, bytesread, ba.length - bytesread)) > 0) { bytesread += retval; } } catch (IOException ioe) { throw new MissingResourceException( "Failed to read in value for '" + key + "': " + ioe, RefCapablePropertyResourceBundle.class.getName(), key); } } finally { try { inputStream.close(); } catch (IOException ioe) { System.err.println("Failed to close input stream: " + ioe); } } if (bytesread != ba.length) { throw new MissingResourceException( "Didn't read all bytes. Read in " + bytesread + " bytes out of " + ba.length + " bytes for key '" + key + "'", RefCapablePropertyResourceBundle.class.getName(), key); } try { return new String(ba, Charset.forName("ISO-8859-1")); } catch (Throwable re) { throw new MissingResourceException( "Value for key '" + key + "' too big to convert to String. " + "Please run the program with more RAM " + "(try Java -Xm* switches).: " + re, RefCapablePropertyResourceBundle.class.getName(), key); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy