/*
* 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;
import net.jini.config.ConfigurationNotFoundException;
/**
* 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[] { new Integer(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.
*
* @author Sun Microsystems, Inc.
* @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));
}
}