/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.shell.syntax;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
/**
* Loads arguments from a syntax extension point descriptor.
*
* In order to provide the benefits of JNode's parsing, completion and help
* systems, a command requires an {@link ArgumentBundle} to parse a
* {@link org.jnode.shell.CommandLine}. Even if that command is not actually
* a {@link org.jnode.shell.Command}, a description of {@code Argument}s can
* be defined and used.
*
* Argument bundle descriptions are provided as sibling elements to syntax nodes
* and are defined with {@code argument-bundle} tags. This tag requires than an {@code alias}
* attribute be given. {@code argument-bundle} accepts {@code argument} tags that provide
* definitions for constructing arguments. It also allows a single {@code typedefs} node to
* be supplied as the first child.
*
* A {@code typedefs} block contains {@code typedef} tags with a mapping between an
* arbitrary string, and a fully qualified class name for an {@code Argument} using
* the {@code name} and {@code value} attributes respectively. Although the {@code name}
* attribute has no restrictions as to what is used, the generally accepted format, for
* clarity in the descriptor, is the name of the argument class. For example a typedef
* for {@code org.jnode.shell.syntax.FileArgument} would be:
*
* <typedef name="FileArgument" value="org.jnode.shell.syntax.FileArgument">
*
* The rest of the child nodes to {@code argument-bundle} must be {@code argument} tags.
* {@code argument} tags require a 'label' and {@code type} attribute. The 'label' is the
* identified which maps the {@code Argument} to a node in the syntax. The {@code type} is
* the fully qualified class name of the {@code Argument}, or the name of a {@code typedef}.
*
* An {@code argument} tag may also contain {@code param} tags to specify more paramaters for an
* {@code Argument}s constructor. The {@code param} tags must be specified in the order
* in which they occur in the constructor. Each {@code param} tag specified two required
* attributes, a {@code type} which tells the kind of the paramater, and a value to
* supply the constuctor upon insantiation. The value must be parseable to the
* given type. If an int type is supplied, but the value cannot be parsed as
* an {@code int} than the parsing will fail.
*
* Currently the only supported types are int, String and flags.
*
* The {@code flags} type is a special type that fills in an {@code Argument}'s flags
* paramater with something other than 0 (None). If the defined argument does
* not set any non-default flags, it is recommended that no {@code param} be supplied
* for clarity in the descriptor.
*
* @see Argument
* @see ArgumentBundle
* @author chris boertien
*/
public class ArgumentSpecLoader {
private Map<String, String> typeDefs;
/**
* Parses a list of {@link Argument}s as {@code ArgumentSpec} objects.
*
* @return an array of {@code ArgumentSpec}s
* @throws SyntaxFailureException if there was an error in the spec.
*/
public ArgumentSpec<?>[] loadArguments(SyntaxSpecAdapter element) {
String alias = element.getAttribute("alias");
if (alias == null) {
throw new SyntaxFailureException("'argument-bundle' element has no 'alias' attribute");
}
int numArgs = element.getNosChildren();
int start = 0;
if (numArgs > 0) {
if (element.getChild(0).getName().equals("typedefs")) {
numArgs--;
start++;
doTypeDefs(element.getChild(0));
}
if (numArgs > 0) {
ArgumentSpec<?>[] args = new ArgumentSpec[numArgs];
for (int i = 0; i < numArgs; i++) {
args[i] = doLoad(element.getChild(i + start));
}
return args;
}
}
throw new SyntaxFailureException("No arguments found in 'argument-bundle' node for : " + alias);
}
/**
* Parses typedefs used to map a String to a fully qualified class
* name for an argument.
*/
private void doTypeDefs(SyntaxSpecAdapter element) {
int numTypeDefs = element.getNosChildren();
if (numTypeDefs > 0) {
typeDefs = new HashMap<String, String>(numTypeDefs);
for (int i = 0; i < numTypeDefs; i++) {
String name = element.getChild(i).getAttribute("name");
String value = element.getChild(i).getAttribute("value");
if (name == null || value == null) {
throw new SyntaxFailureException("Missing value or name in 'typedef'");
}
typeDefs.put(name, value);
}
} else {
throw new SyntaxFailureException("'typedefs' found, but no 'typedef' nodes");
}
}
/**
* Parses an argument.
*/
@SuppressWarnings("unchecked")
private ArgumentSpec<?> doLoad(SyntaxSpecAdapter element) {
if (!element.getName().equals("argument")) {
throw new SyntaxFailureException("Not a valid child of 'argument-bundle': " + element.getName());
}
String type = element.getAttribute("type");
String label = element.getAttribute("label");
int numParams = element.getNosChildren();
Object[] params;
Class<?>[] paramTypes;
if (numParams > 0) {
params = new Object[numParams + 1];
paramTypes = new Class<?>[numParams + 1];
for (int i = 0; i < numParams; i++) {
parseParam(element.getChild(i), i + 1, params, paramTypes);
}
} else {
params = new Object[2];
paramTypes = new Class<?>[2];
params[1] = 0;
paramTypes[1] = int.class;
}
params[0] = label;
paramTypes[0] = String.class;
try {
if (typeDefs != null && typeDefs.containsKey(type)) {
type = typeDefs.get(type);
}
Class<? extends Argument<?>> argClass = (Class<? extends Argument<?>>) Class.forName(type);
Constructor<? extends Argument<?>> ctor = argClass.getConstructor(paramTypes);
return new ArgumentSpec(ctor, params);
} catch (ClassCastException ex) {
throw new SyntaxFailureException("'type' is not a subclass of Argument: " + type);
} catch (ClassNotFoundException ex) {
throw new SyntaxFailureException("'type' could not be found: " + type);
} catch (Exception ex) {
throw new SyntaxFailureException("'type' could not be instantiated, invalid constructor");
}
}
/**
* Parses a paramater for the constructor of an argument.
*
* Currently acceptable types: flags, int, String
*/
private void parseParam(SyntaxSpecAdapter element, int i, Object[] params, Class<?>[] paramTypes) {
if (!element.getName().equals("param")) {
throw new SyntaxFailureException("'argument' contains a child that is not a 'param': " + element.getName());
}
String type = element.getAttribute("type");
String value = element.getAttribute("value");
if (type == null) {
throw new SyntaxFailureException("'type' cannot be null for node 'param'");
}
if (value == null) {
throw new SyntaxFailureException("'value' cannot be null for node 'param'");
}
if (type.equals("String")) {
params[i] = value;
paramTypes[i] = String.class;
} else if (type.equals("int")) {
try {
params[i] = Integer.parseInt(value);
} catch (NumberFormatException ex) {
throw new SyntaxFailureException("'param' node declare 'type' int, but 'value' was not a valid int");
}
paramTypes[i] = int.class;
} else if (type.equals("flags")) {
params[i] = parseFlags(value);
paramTypes[i] = int.class;
} else {
throw new SyntaxFailureException("The given 'type' is not supported: " + type);
}
}
public static class ArgumentSpec<T extends Argument<?>> {
private Constructor<T> ctor;
private Object[] params;
private ArgumentSpec(Constructor<T> ctor, Object[] params) {
this.ctor = ctor;
this.params = params;
}
Argument<?> instantiate() throws Exception {
return (Argument<?>) ctor.newInstance(params);
}
}
// this is nothing short of a hack, don't pay it much attention for now
private int parseFlags(String flags) {
String[] nameList = flags.trim().split("\\s*,\\s*");
int res = 0;
for (String name : nameList) {
if (name != null && name.length() > 0) {
if (name.equals("MANDATORY")) {
res |= Argument.MANDATORY;
} else if (name.equals("OPTIONAL")) {
res |= Argument.OPTIONAL;
} else if (name.equals("SINGLE")) {
res |= Argument.SINGLE;
} else if (name.equals("MULTIPLE")) {
res |= Argument.MULTIPLE;
} else if (name.equals("EXISTING")) {
res |= Argument.EXISTING;
} else if (name.equals("NONEXISTENT")) {
res |= Argument.NONEXISTENT;
} else {
throw new IllegalArgumentException("unknown flag name '" + name + "'");
}
}
}
return res;
}
}