/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 com.sun.jini.tool;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationFile;
import net.jini.config.ConfigurationProvider;

/**
 * Checks the format of the source for a {@link ConfigurationFile}. The source
 * is specified with either a file, URL, or standard input, as well as with
 * override options. The checks include syntax and static type checking, and
 * require access to any application types mentioned in the source. <p>
 *
 * The following items are discussed below:
 *
 * <ul>
 * <li> <a href="#entry_desc">Entry description files</a>
 * <li> {@linkplain #main Command line options}
 * <li> <a href="#examples">Examples for running CheckConfigurationFile</a>
 * </ul> <p>
 *
 * <a name="entry_desc">
 * <h3>Entry description files</h3>
 * </a>
 *
 * Checking of the source can be controlled by specifying one or more entry
 * description files, each listing the names and types of entries that are
 * allowed to appear in the source. Each entry description file is treated as a
 * {@link Properties} source file, where each key is the fully qualified name
 * of an entry (<code><i>component</i>.<i>name</i></code>) and each value
 * specifies the expected type for that entry. Types should be specified in
 * normal source code format, except that whitespace is not permitted between
 * tokens. Types in the <code>java.lang</code> package may be unqualified, but
 * fully qualified names must be used for other types (<code>import</code>
 * statements are not supported).  If any entry description files are supplied,
 * then any public entry that appears in the source being checked, whose fully
 * qualified name does not appear in any entry description file, or whose
 * actual type is not assignable to the expected type, is treated as an
 * error. <p>
 *
 * Entry description files for all of the Apache River release services and utilities
 * are provided in the <code>configentry</code> subdirectory beneath the
 * top-level directory of the Apache River release installation. <p>
 *
 * Here is a sample entry description file:
 *
 * <blockquote>
 * <pre>
 * comp.foo Integer[]
 * comp.bar net.jini.core.constraint.MethodConstraints
 * comp.baz long
 * </pre>
 * </blockquote>
 *
 * Here is an associated sample configuration file:
 *
 * <blockquote>
 * <pre>
 * import net.jini.constraint.*;
 * import net.jini.core.constraint.*;
 * comp {
 *     foo = new Integer[] { Integer.valueOf(3) };
 *     bar = new BasicMethodConstraints(
 *               new InvocationConstraints(Integrity.YES, null));
 *     baz = 33L;
 * }
 * </pre>
 * </blockquote> <p>
 *
 * <a name="examples">
 * <h3>Examples for running CheckConfigurationFile</h3>
 * </a>
 *
 * This utility can be run from the {@linkplain #main command line}, or by
 * calling the {@link #check(String, ClassLoader, String[], String,
 * PrintStream)} or {@link #check(ConfigurationFile, Properties, ClassLoader,
 * PrintStream)} methods. <p>
 *
 * An example command line usage is:
 *
 * <blockquote>
 * <pre>
 * java -jar <var><b>install_dir</b></var>/lib/checkconfigurationfile.jar \
 *      -cp <var><b>install_dir</b></var>/lib/norm.jar:<var><b>install_dir</b></var>/lib/jsk-platform.jar \
 *      -entries <var><b>install_dir</b></var>/configentry/norm-transient \
 *      <var><b>your-norm.config</b></var>
 * </pre>
 * </blockquote>
 * 
 * where <var><b>install_dir</b></var> is the directory where the Apache River release
 * is installed, and <var><b>your-norm.config</b></var> is a configuration
 * source file intended for use with the transient {@linkplain
 * com.sun.jini.norm Norm} service implementation. This command will print out
 * any problems that it detects in the configuration file, including entries
 * that are not recognized or have the wrong type for the Norm service.
 *
 * 
 * @see ConfigurationFile
 * @since 2.0
 */
public class CheckConfigurationFile  {

    /** The primitive types */
    private static final Class[] primitives = {
	Boolean.TYPE, Byte.TYPE, Character.TYPE, Short.TYPE, Integer.TYPE,
	Long.TYPE, Float.TYPE, Double.TYPE
    };

    private static final String RESOURCE =
	"META-INF/services/" + Configuration.class.getName();

    private static ResourceBundle resources;
    private static boolean resinit = false;

    /* This class is not instantiable */
    private CheckConfigurationFile() { }

    /**
     * Command line interface for checking the format of source and override
     * options for a {@link ConfigurationFile}, printing messages to
     * <code>System.err</code> for any errors found. If errors are found,
     * continues to check the rest of the source and overrides, and then calls
     * <code>System.exit</code> with a non-<code>zero</code> argument. <p>
     *
     * The command line arguments are:
     *
     * <pre>
     * [ -cp <var><b>classpath</b></var> ] [ -entries <var><b>entrydescs</b></var> ] <var><b>location</b></var> [ <var><b>option</b></var>... ]
     * </pre>
     * or
     * <pre>
     * [ -cp <var><b>classpath</b></var> ] [ -entries <var><b>entrydescs</b></var> ] -stdin [ <var><b>location</b></var> [ <var><b>option</b></var>... ] ]
     * </pre>
     * or
     * <pre>
     * -help
     * </pre>
     *
     * If the only argument is <code>-help</code>, a usage message is
     * printed. <p>
     *
     * The <var><b>classpath</b></var> value for the <code>-cp</code> option
     * specifies one or more directories and zip/JAR files, separated by the
     * {@linkplain File#pathSeparatorChar path separator character}, where the
     * application classes are located.  A class loader that loads classes from
     * this path will be created, with the extension class loader as its
     * parent. If this option is not specified, the system class loader is used
     * instead. <p>
     *
     * The <var><b>entrydescs</b></var> value for the <code>-entries</code>
     * option specifies one or more entry description files, separated by the
     * path separator character. <p>
     *
     * The <var><b>location</b></var> argument specifies the source file to be
     * checked. If the <code>-stdin</code> option is used, then the actual
     * source data will be read from standard input, and any
     * <var><b>location</b></var> argument is simply used for identification
     * purposes in error messages. <p>
     *
     * The remaining arguments specify any entry override values that should be
     * passed to the <code>ConfigurationFile</code> constructor. <p>
     *
     * The class loader obtained above is used to resolve all expected types
     * specified in the entry description files, and to obtain the
     * configuration provider. The configuration provider class is found from
     * the class loader in the same manner as specified by {@link
     * ConfigurationProvider}.  The resulting class must be {@link
     * ConfigurationFile} or a subclass; if it is a subclass, it must have a
     * public constructor with three parameters of type: {@link Reader},
     * <code>String[]</code>, and {@link ClassLoader}. An instance of the
     * provider is created by passing that constructor a <code>Reader</code>
     * for the source file to be checked, the location and entry override
     * values, and the class loader.
     */
    public static void main(String[] args) {
	if (args.length == 0) {
	    usage();
	} else if (args.length == 1 && "-help".equals(args[0])) {
	    print(System.err, "checkconfig.usage", File.pathSeparator);
	    return;
	}
	String classPath = null;
	String entriesPath = null;
	boolean stdin = false;
	int i = 0;
	while (i < args.length) {
	    if ("-cp".equals(args[i])) {
		if (args.length < i + 2) {
		    usage();
		}
		classPath = args[i + 1];
		i += 2;
	    } else if ("-entries".equals(args[i])) {
		if (args.length < i + 2) {
		    usage();
		}
		entriesPath = args[i + 1];
		i += 2;
	    } else if ("-stdin".equals(args[i])) {
		stdin = true;
		i++;
	    } else {
		break;
	    }
	}
	if (!stdin && i == args.length) {
	    usage();
	}
	ClassLoader loader = ClassLoader.getSystemClassLoader();
	if (classPath != null) {
	    loader = loader.getParent();
	}
	String[] configOptions = new String[args.length - i];
	System.arraycopy(args, i, configOptions, 0, configOptions.length);
	boolean ok = check(classPath, loader, stdin, configOptions,
			   entriesPath, System.err);
	if (!ok) {
	    System.exit(1);
	}
    }

    private static void usage() {
	print(System.err, "checkconfig.usage", File.pathSeparator);
	System.exit(1);
    }

    /**
     * Returns information about the specified permitted entries, returning
     * null if there is a problem loading properties from the files.
     */
    private static Properties getEntries(String files, PrintStream err) {
	Properties entries = new Properties();
	StringTokenizer tokens =
	    new StringTokenizer(files, File.pathSeparator);
	while (tokens.hasMoreTokens()) {
	    String file = tokens.nextToken();
	    InputStream in = null;
	    try {
		in = new BufferedInputStream(new FileInputStream(file));
		entries.load(in);
	    } catch (FileNotFoundException e) {
		print(err, "checkconfig.notfound", file);
		return null;
	    } catch (Throwable t) {
		print(err, "checkconfig.read.err",
		      new String[]{file, t.getClass().getName(),
				   t.getLocalizedMessage()});
		t.printStackTrace(err);
		return null;
	    } finally {
		if (in != null) {
		    try {
			in.close();
		    } catch (IOException e) {
		    }
		}
	    }
	}
	return entries;
    }

    /**
     * Checks the format of a configuration source file. Returns
     * <code>true</code> if there are no errors, and <code>false</code>
     * otherwise. <p>
     *
     * The <code>classPath</code> argument specifies one or more directories
     * and zip/JAR files, separated by the {@linkplain File#pathSeparatorChar
     * path separator character}, where the application classes are located.  A
     * class loader that loads classes from this path will be created, with
     * <code>loader</code> as its parent. The <code>ConfigurationFile</code> is
     * created with this class loader, and all expected types specified in
     * entry description files are resolved in this class loader. If
     * <code>classPath</code> is <code>null</code>, then <code>loader</code> is
     * used instead. <p>
     *
     * The class loader is used to resolve all expected types specified in the
     * entry description files, and to obtain the configuration provider. The
     * configuration provider class is found from the class loader in the same
     * manner as specified by {@link ConfigurationProvider}.  The resulting
     * class must be {@link ConfigurationFile} or a subclass; if it is a
     * subclass, it must have a public constructor with three parameters of
     * type: {@link Reader}, <code>String[]</code>, and {@link
     * ClassLoader}. An instance of the provider is created by passing that
     * constructor a <code>Reader</code> for the source file to be checked,
     * the location and entry override values, and the class loader.
     *
     * @param classPath the search path for application classes, or
     * <code>null</code> to use the specified class loader
     * @param loader the parent class loader to use for application classes if
     * <code>classPath</code> is not <code>null</code>, otherwise the class
     * loader to use for resolving application classes
     * @param configOptions the configuration source file to check, plus any
     * entry overrides
     * @param entriesPath one or more entry description files, separated by the
     * path separator character, or <code>null</code>
     * @param err the stream to use for printing errors
     * @return <code>true</code> if there are no errors, <code>false</code>
     * otherwise
     * @throws NullPointerException if <code>loader</code>,
     * <code>configOptions</code>, or <code>err</code> is <code>null</code>
     * @see #check(ConfigurationFile, Properties, ClassLoader, PrintStream)
     */
    public static boolean check(String classPath,
				ClassLoader loader,
				String[] configOptions,
				String entriesPath,
				PrintStream err)
    {
	if (loader == null || configOptions == null || err == null) {
	    throw new NullPointerException();
	}
	return check(classPath, loader, false, configOptions, entriesPath,
		     err);
    }

    /**
     * Checks the format of a configuration source file, using classes loaded
     * from classPath and loader, requiring entries to match the names and
     * values from entriesPath (unless entriesPath is null).  If stdin is
     * true, reads the configuration source from standard input.
     */
    private static boolean check(String classPath,
				 ClassLoader loader,
				 boolean stdin,
				 String[] configOptions,
				 String entriesPath,
				 PrintStream err)
    {
	if (classPath != null) {
	    StringTokenizer st = new StringTokenizer(classPath,
						     File.pathSeparator);
	    URL[] urls = new URL[st.countTokens()];
	    for (int i = 0; st.hasMoreTokens(); i++) {
		String elt = st.nextToken();
		try {
		    urls[i] = new File(elt).toURI().toURL();
		} catch (MalformedURLException e) {
		    print(err, "checkconfig.classpath", elt);
		    return false;
		}
	    }
	    loader = URLClassLoader.newInstance(urls, loader);
	}
	Properties entries = null;
	if (entriesPath != null) {
	    entries = getEntries(entriesPath, err);
	    if (entries == null) {
		return false;
	    }
	}
	String location;
	if (configOptions.length == 0) {
	    location = "(stdin)";
	} else {
	    configOptions = (String[]) configOptions.clone();
	    location = configOptions[0];
	    configOptions[0] = "-";
	}
	Constructor configCons = getProviderConstructor(loader, err);
	if (configCons == null) {
	    return false;
	}
	try {
	    InputStream in;
	    if (stdin) {
		in = System.in;
	    } else if ("-".equals(location)) {
		in = new ByteArrayInputStream(new byte[0]);
	    } else {
		try {
		    URL url = new URL(location);
		    in = url.openStream();
		} catch (MalformedURLException e) {
		    in = new FileInputStream(location);
		}
	    }
	    Object config;
	    try {
		config = configCons.newInstance(
				  new Object[]{new InputStreamReader(in),
					       configOptions,
					       loader});
	    } finally {
		if (!stdin) {
		    try {
			in.close();
		    } catch (IOException e) {
		    }
		}
	    }
	    return check(config, entries, loader, err);
	} catch (FileNotFoundException e) {
	    print(err, "checkconfig.notfound", location);
	} catch (Throwable t) {
	    print(err, "checkconfig.read", location, t);
	}
	return false;
    }

    /**
     * Returns the (Reader, String[], ClassLoader) constructor for the
     * resource-specified configuration provider in loader.
     */
    private static Constructor getProviderConstructor(ClassLoader loader,
						      PrintStream err)
    {
	URL resource = null;
	try {
	    for (Enumeration providers = loader.getResources(RESOURCE);
		 providers.hasMoreElements(); )
	    {
		resource = (URL) providers.nextElement();
	    }
	} catch (IOException e) {
	    print(err, "checkconfig.resources", "", e);
	}
	String classname = (resource == null ?
			    ConfigurationFile.class.getName() :
			    getProviderName(resource, err));
	if (classname == null) {
	    return null;
	}
	try {
	    Class provider = Class.forName(classname, false, loader);
	    try {
		if (!Class.forName(ConfigurationFile.class.getName(),
				   false, loader).isAssignableFrom(provider))
		{
		    print(err, "checkconfig.notsubclass", classname);
		    return null;
		}
		return provider.getConstructor(new Class[]{Reader.class,
							   String[].class,
							   ClassLoader.class});
	    } catch (ClassNotFoundException e) {
		print(err, "checkconfig.notsubclass", classname);
	    } catch (NoSuchMethodException e) {
		print(err, "checkconfig.noconstructor", classname);
	    }
	} catch (ClassNotFoundException e) {
	    print(err, "checkconfig.noprovider", classname);
	} catch (Throwable t) {
	    print(err, "checkconfig.provider", classname, t);
	}
	return null;
    }

    /**
     * Returns the configuration provider class name specified in the contents
     * of the URL.
     */
    private static String getProviderName(URL url, PrintStream err) {
	InputStream in = null;
	try {
	    in = url.openStream();
	    BufferedReader reader =
		new BufferedReader(new InputStreamReader(in, "utf-8"));
	    String result = null;
	    while (true) {
		String line = reader.readLine();
		if (line == null) {
		    break;
		}
		int commentPos = line.indexOf('#');
		if (commentPos >= 0) {
		    line = line.substring(0, commentPos);
		}
		line = line.trim();
		int len = line.length();
		if (len != 0) {
		    if (result != null) {
			print(err, "checkconfig.multiproviders",
			      url.toString());
			return null;
		    }
		    result = line;
		}
	    }
	    if (result == null) {
		print(err, "checkconfig.missingprovider", url.toString());
		return null;
	    }
	    return result;
	} catch (IOException e) {
	    print(err, "configconfig.read", url.toString(), e);
	    return null;
	} finally {
	    if (in != null) {
		try {
		    in.close();
		} catch (IOException e) {
		}
	    }
	}
    }

    /**
     * Checks the format of a <code>ConfigurationFile</code>. Returns
     * <code>true</code> if there are no errors, and <code>false</code>
     * otherwise.
     *
     * @param config the <code>ConfigurationFile</code> to check
     * @param entries the entry descriptions to use (where each key is a fully
     * qualified entry name and each value is the expected type), or
     * <code>null</code>
     * @param loader the class loader to use for resolving type names used in
     * the entry descriptions
     * @param err the stream to use for printing errors
     * @return <code>true</code> if there are no errors, <code>false</code>
     * otherwise
     * @throws NullPointerException if <code>config</code>,
     * <code>loader</code>, or <code>err</code> is <code>null</code>
     * @see #check(String, ClassLoader, String[], String, PrintStream)
     */
    public static boolean check(ConfigurationFile config,
				Properties entries,
				ClassLoader loader,
				PrintStream err)
    {
	if (config == null || loader == null || err == null) {
	    throw new NullPointerException();
	}
	return check((Object) config, entries, loader, err);
    }

    /**
     * Checks the entries in config against the descriptions in entries,
     * resolving expected types in loader.
     */
    private static boolean check(Object config,
				 Properties entries,
				 ClassLoader loader,
				 PrintStream err)
    {
	Method getType;
	String[] entryNames;
	try {
	    getType = config.getClass().getMethod("getEntryType",
						  new Class[]{String.class,
							      String.class});
	    Method getNames = config.getClass().getMethod("getEntryNames",
							  null);
	    Set entrySet = (Set) getNames.invoke(config, new Object[0]);
	    entryNames =
		(String[]) entrySet.toArray(new String[entrySet.size()]);
	} catch (Throwable t) {
	    print(err, "checkconfig.unexpected", "", t);
	    return false;
	}
	Arrays.sort(entryNames);
	boolean ok = true;
	for (int i = 0; i < entryNames.length; i++) {
	    String entryName = entryNames[i];
	    String expectedTypeName = entries != null
		? entries.getProperty(entryName) : null;
	    if (entries != null && expectedTypeName == null) {
		print(err, "checkconfig.unknown", entryName);
		ok = false;
	    }
	    try {
		int dot = entryName.lastIndexOf('.');
		String component = entryName.substring(0, dot);
		String name = entryName.substring(dot + 1);
		Class type = (Class) getType.invoke(config,
						    new Object[]{component,
								 name});
		if (expectedTypeName != null) {
		    try {
			Class expectedType =
			    findClass(expectedTypeName, loader);
			if (!isAssignableFrom(expectedType, type)) {
			    print(err, "checkconfig.mismatch",
				  new String[]{entryName, typeName(type),
					       typeName(expectedType)});
			    ok = false;
			}
		    } catch (ClassNotFoundException e) {
			print(err, "checkconfig.expect.fail",
			      new String[]{entryName, e.getMessage()});
			ok = false;
		    } catch (Throwable t) {
			print(err, "checkconfig.expect.err",
			      new String[]{entryName, expectedTypeName,
					   t.getClass().getName(),
					   t.getLocalizedMessage()});
			t.printStackTrace(err);
			ok = false;
		    }
		}
	    } catch (Throwable t) {
		print(err, "checkconfig.actual", entryName, t);
		ok = false;
	    }
	}
	return ok;
    }

    /**
     * Returns the type with the specified name, including primitives and
     * arrays, and checking the java.lang package for unqualified names.  This
     * method supports both internal names ('[I', '[Ljava.lang.Integer;',
     * 'java.util.Map$Entry') and source level name ('int[]', 'Integer[]',
     * 'java.util.Map.Entry').
     */
    private static Class findClass(String name, ClassLoader loader)
	throws ClassNotFoundException
    {
	if (name.indexOf('.') < 0) {
	    for (int i = primitives.length; --i >= 0; ) {
		if (name.equals(primitives[i].getName())) {
		    return primitives[i];
		}
	    }
	}
	try {
	    return Class.forName(name, false, loader);
	} catch (ClassNotFoundException notFound) {
	    int bracket = name.indexOf('[');
	    if (bracket > 0) {
		/* Array */
		int dims = 0;
		int len = name.length();
		for (int i = bracket; i < len; i += 2, dims++) {
		    if (name.charAt(i) != '['
			|| i + 1 >= len
			|| name.charAt(i + 1) != ']')
		    {
			/* Invalid array class name */
			throw notFound;
		    }
		}
		try {
		    Class base = findClass(name.substring(0, bracket), loader);
		    return Array.newInstance(base, new int[dims]).getClass();
		} catch (ClassNotFoundException e) {
		}
	    } else if (name.indexOf('.') < 0) {
		/* Try java.lang package */
		try {
		    return findClass("java.lang." + name, loader);
		} catch (ClassNotFoundException e) {
		}
	    } else {
		/* Try substituting '$' for '.' to look for nested classes */
		int dot;
		while ((dot = name.lastIndexOf('.')) >= 0) {
		    name = name.substring(0, dot) + '$' +
			name.substring(dot + 1);
		    try {
			return Class.forName(name, false, loader);
		    } catch (ClassNotFoundException e) {
		    }
		}
	    }
	    throw notFound;
	}
    }

    /** Returns the name of a type, in source code format. */
    private static String typeName(Class type) {
	if (type == null) {
	    return "null";
	}
	StringBuffer buf = new StringBuffer();
	if (type.isArray()) {
	    Class component;
	    while ((component = type.getComponentType()) != null) {
		buf.append("[]");
		type = component;
	    }
	}
	return type.getName().replace('$', '.') + buf;
    }

    /**
     * Checks if an object of type source can be assigned to a variable of type
     * dest, where source is null for a null object.
     */
    private static boolean isAssignableFrom(Class dest, Class source) {
	if (dest.isPrimitive()) {
	    return source == dest;
	} else {
	    return source == null || dest.isAssignableFrom(source);
	}
    }

    /**
     * Returns a message from the resource bundle.
     */
    private static synchronized String getString(String key, PrintStream err) {
	if (!resinit) {
	    try {
		resinit = true;
		resources = ResourceBundle.getBundle(
			       "com.sun.jini.tool.resources.checkconfig");
	    } catch (MissingResourceException e) {
		e.printStackTrace(err);
	    }
	}
	try {
	    return resources != null ? resources.getString(key) : null;
	} catch (MissingResourceException e) {
	    return null;
	}
    }

    /**
     * If t is a ConfigurationException, uses the key keyPrefix+".fail",
     * with source as the value for {0} and the localized message of t
     * as the value for {1}. Otherwise, uses the key keyPrefix+".err",
     * with source as the value for {0}, the exception class name as the
     * value for {1}, and the localized message of t as the value for {2}.
     */
    private static void print(PrintStream err,
			      String keyPrefix,
			      String source,
			      Throwable t)
    {
	if (t instanceof InvocationTargetException) {
	    t = t.getCause();
	}
	if (t.getClass().getName().equals(
				      ConfigurationException.class.getName()))
	{
	    if (t.getCause() == null) {
		print(err, keyPrefix + ".fail",
		      new String[]{source, t.getLocalizedMessage()});
		return;
	    } else {
		t = t.getCause();
	    }
	}
	print(err, keyPrefix + ".err",
	      new String[]{source, t.getClass().getName(),
			   t.getLocalizedMessage()});
	t.printStackTrace(err);
    }

    private static void print(PrintStream err, String key, String val) {
	print(err, key, new String[]{val});
    }

    private static void print(PrintStream err, String key, String[] vals) {
	String fmt = getString(key, err);
	if (fmt == null)
	    fmt = "no text found: \"" + key + "\" {0}";
	err.println(MessageFormat.format(fmt, vals));
    }
}
