
org.jsimpledb.parse.expr.AtomExprParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jsimpledb-parse Show documentation
Show all versions of jsimpledb-parse Show documentation
JSimpleDB classes for parsing Java expressions.
The newest version!
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb.parse.expr;
import java.beans.BeanInfo;
import java.beans.IndexedPropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.stream.Stream;
import org.jsimpledb.parse.ParseException;
import org.jsimpledb.parse.ParseSession;
import org.jsimpledb.parse.ParseUtil;
import org.jsimpledb.parse.Parser;
import org.jsimpledb.parse.SpaceParser;
import org.jsimpledb.parse.func.Function;
import org.jsimpledb.util.ParseContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
/**
* Parses atomic Java expressions such as parenthesized expressions, {@code new} expressions,
* session function calls, identifiers (e.g., lambda method parameter names), and literals.
*
*
* Includes these extensions:
*
* - Access to Java bean methods via "property syntax", e.g., {@code foo.name = "fred"} means {@code foo.setName("fred")}
* - Access database object fields via "property syntax" given an object ID, e.g.,
@fc21bf6d8930a215.name
*
*/
public class AtomExprParser implements Parser {
public static final AtomExprParser INSTANCE = new AtomExprParser();
private final SpaceParser spaceParser = new SpaceParser();
@Override
public Node parse(ParseSession session, ParseContext ctx, boolean complete) {
// Note starting point for back-tracking
final int start = ctx.getIndex();
// Handle parenthesized expression
if (ctx.tryLiteral("(")) {
this.spaceParser.parse(ctx, complete);
final Node node = ExprParser.INSTANCE.parse(session, ctx, complete);
this.spaceParser.parse(ctx, complete);
if (!ctx.tryLiteral(")"))
throw new ParseException(ctx).addCompletion(") ");
return node;
}
// Handle literals
try {
return LiteralExprParser.INSTANCE.parse(session, ctx, complete);
} catch (ParseException e) {
ctx.setIndex(start);
}
// Handle "new" expressions
if (ctx.tryPattern("new(?!\\p{javaJavaIdentifierPart})") != null) {
// Get base class name
new SpaceParser(true).parse(ctx, complete);
final String className = ctx.matchPrefix(LiteralExprParser.IDENTS_AND_DOTS_PATTERN).group().replaceAll("\\s+", "");
final ClassNode classNode = ClassNode.parse(ctx, className, true);
// Handle tab-completion
if (complete && ctx.isEOF()) {
try {
session.resolveClass(className, true);
} catch (IllegalArgumentException e) {
throw new ParseException(ctx).addCompletions(AtomExprParser.getClassNameCompletions(session, className));
}
}
if (className.equals("void"))
throw new ParseException(ctx, "illegal instantiation of void");
// Get array dimensions, if any, including length initialization expressions
final ArrayList dimensions = new ArrayList<>();
boolean sawNull = false;
while (true) {
ctx.skipWhitespace();
if (!ctx.tryLiteral("["))
break;
ctx.skipWhitespace();
if (ctx.tryLiteral("]")) {
dimensions.add(null);
sawNull = true;
continue;
}
if (!sawNull) {
dimensions.add(ExprParser.INSTANCE.parse(session, ctx, complete));
ctx.skipWhitespace();
}
if (!ctx.tryLiteral("]"))
throw new ParseException(ctx, "expected `]'").addCompletion("]");
}
final int numDimensions = dimensions.size();
// Handle non-array instantiation
if (numDimensions == 0) {
// Parse parameters
if (!ctx.tryLiteral("("))
throw new ParseException(ctx, "expected `('").addCompletion("(");
ctx.skipWhitespace();
final List paramNodes = AtomExprParser.parseParams(session, ctx, complete);
// Return constructor invocation node
return new ConstructorInvokeNode(classNode, paramNodes);
}
// Check number of dimensions
if (numDimensions > 255)
throw new ParseException(ctx, "too many array dimensions (" + numDimensions + " > 255)");
// Handle empty or initialized array instantiation expression
return dimensions.get(0) != null ?
new EmptyArrayNode(classNode, dimensions) :
new LiteralArrayNode(classNode, numDimensions,
LiteralArrayNode.parseArrayLiteral(session, ctx, complete, numDimensions));
} else
ctx.setIndex(start);
// Handle session function invocation - distguished by a single identifier, followed by '('
final Matcher functionMatcher = ctx.tryPattern("(" + ParseUtil.IDENT_PATTERN + ")\\s*\\(");
if (functionMatcher != null) {
// Find function
final String functionName = functionMatcher.group(1);
final Function function = session.getFunctions().get(functionName);
if (function == null) {
throw new ParseException(ctx, "unknown function `" + functionName + "()'")
.addCompletions(ParseUtil.complete(session.getFunctions().keySet(), functionName));
}
// Parse function parameters
ctx.skipWhitespace();
final Object params = function.parseParams(session, ctx, complete);
// Return node that applies the function to the parameters
return new Node() {
@Override
public Value evaluate(ParseSession session) {
return function.apply(session, params);
}
@Override
public Class> getType(ParseSession session) {
return Object.class;
}
};
}
// Handle plain identifier - delegate to standalone identifier parser, if any
final Parser extends Node> identifierParser = session.getIdentifierParser();
if (identifierParser != null) {
try {
final Node node = identifierParser.parse(session, ctx, complete);
ctx.skipWhitespace();
return node;
} catch (ParseException e) {
if (complete && ctx.isEOF())
throw e;
ctx.setIndex(start);
}
}
// Handle unbound method references
final Matcher methodMatcher = ctx.tryPattern(
"(" + LiteralExprParser.CLASS_NAME_PATTERN + ")\\s*::\\s*(" + ParseUtil.IDENT_PATTERN + ")");
if (methodMatcher != null) {
// Parse class and method name
final ClassNode classNode = ClassNode.parse(ctx, methodMatcher.group(1), false);
final String methodName = methodMatcher.group(2);
// Support tab-completion of method names
if (complete && ctx.isEOF()) {
Class> type = null;
try {
type = classNode.resolveClass(session);
} catch (IllegalArgumentException e) {
// ignore
}
if (type != null) {
final HashSet validMethodNames = new HashSet<>();
validMethodNames.add("new");
for (Method method : type.getMethods())
validMethodNames.add(method.getName());
if (!validMethodNames.contains(methodName)) {
throw new ParseException(ctx, "unknown method `" + methodName + "()' in " + type)
.addCompletions(ParseUtil.complete(validMethodNames, methodName));
}
}
}
// Return unbound method reference
return new UnboundMethodReferenceNode(classNode, methodName);
}
// Handle tab-completion of "Foo" and "Foo.", assuming "Foo" is a class name (or prefix of a class name)
if (complete) {
final Matcher nameDotMatcher = ctx.tryPattern("(" + ParseUtil.IDENT_PATTERN + ")\\s*(\\.\\s*)?$");
if (nameDotMatcher != null) {
final String className = nameDotMatcher.group(1);
if (nameDotMatcher.group(2) == null)
throw new ParseException(ctx).addCompletions(AtomExprParser.getClassNameCompletions(session, className));
try {
final Class> clazz = session.resolveClass(className, false);
throw new ParseException(ctx).addCompletions(AtomExprParser.getStaticMemberCompletions(clazz, ""));
} catch (IllegalArgumentException e) {
ctx.setIndex(start); // class not found
}
}
}
// Handle static method invocation and field access
final Matcher staticMatcher = ctx.tryPattern(
"(" + ParseUtil.IDENT_PATTERN + ")\\s*\\.\\s*(" + ParseUtil.IDENT_PATTERN + ")");
if (staticMatcher != null) {
// Parse first two identifiers
final StringBuilder idents = new StringBuilder(staticMatcher.group(1)); // class name so far
String member = staticMatcher.group(2); // class member name
// Keep parsing identifiers as long as we can; after each identifier, try to resolve a class name
final ArrayList indexList = new ArrayList<>();
final ArrayList> classList = new ArrayList<>();
final ArrayList memberList = new ArrayList<>();
Class> cl;
while (true) {
try {
cl = session.resolveClass(idents.toString(), false);
} catch (IllegalArgumentException e) {
cl = null;
}
classList.add(cl);
indexList.add(ctx.getIndex());
memberList.add(member);
final Matcher matcher = ctx.tryPattern("\\s*\\.\\s*(" + ParseUtil.IDENT_PATTERN + ")");
if (matcher == null)
break;
idents.append('.').append(member);
member = matcher.group(1);
}
ctx.skipWhitespace();
// Use the longest class name resolved, if any
for (int i = classList.size() - 1; i >= 0; i--) {
if ((cl = classList.get(i)) != null) {
ctx.setIndex(indexList.get(i));
member = memberList.get(i);
break;
}
}
// Check if class was found; if not, handle tab-completion of class name
if (cl == null) {
final ParseException e = new ParseException(ctx, "unknown class `" + idents + "'");
if (complete && ctx.isEOF())
e.addCompletions(AtomExprParser.getClassNameCompletions(session, idents.toString()));
throw e;
}
// Handle tab-completion of static class members
if (complete && ctx.isEOF())
throw new ParseException(ctx).addCompletions(AtomExprParser.getStaticMemberCompletions(cl, member));
// Handle static method invocation
if (ctx.tryPattern("\\s*\\(") != null)
return new MethodInvokeNode(cl, member, AtomExprParser.parseParams(session, ctx, complete));
// Handle static field access
try {
return new ConstNode(new StaticFieldValue(AtomExprParser.findField(cl, member, true)));
} catch (NoSuchFieldException e) {
throw new ParseException(ctx, e.getMessage());
}
}
// Can't parse this
throw new ParseException(ctx);
}
// Find field
static Field findField(Class> cl, String fieldName, boolean isStatic) throws NoSuchFieldException {
final Field bestMatch = AtomExprParser.findField(cl, fieldName, null, isStatic);
if (bestMatch == null)
throw new NoSuchFieldException("class `" + cl.getName() + "' has no field named `" + fieldName + "'");
if (((bestMatch.getModifiers() & Modifier.STATIC) != 0) != isStatic) {
throw new NoSuchFieldException("field `" + fieldName
+ "' in class `" + cl.getName() + "' is not " + (isStatic ? "a static" : "an instance") + " field");
}
try {
bestMatch.setAccessible(true);
} catch (SecurityException e) {
// ignore
}
return bestMatch;
}
// Helper method for findField()
private static Field findField(Class> cl, String fieldName, Field bestSoFar, boolean isStatic) {
// Find field with the given name declared in class, if any
final int mask = Modifier.STATIC;
final int flags = isStatic ? Modifier.STATIC : 0;
for (Field candidate : cl.getDeclaredFields()) {
if (!candidate.getName().equals(fieldName))
continue;
if (bestSoFar == null || (bestSoFar.getModifiers() & mask) != flags)
bestSoFar = candidate;
if (bestSoFar != null && (bestSoFar.getModifiers() & mask) == flags)
return bestSoFar;
break;
}
// Recurse on supertypes
if (cl.getSuperclass() != null)
bestSoFar = AtomExprParser.findField(cl.getSuperclass(), fieldName, bestSoFar, isStatic);
for (Class> iface : cl.getInterfaces())
bestSoFar = AtomExprParser.findField(iface, fieldName, bestSoFar, isStatic);
// Done
return bestSoFar;
}
/**
* Get all class name completions for {@code some.class.Name...}.
*/
static Stream getClassNameCompletions(ParseSession session, String name) {
// Use a separate class to avoid exception if Spring classes are not available
try {
return new ClassNameCompletion(name).findCompletions(session);
} catch (NoClassDefFoundError e) {
return Stream.empty();
}
}
/**
* Get all public static member name completions for {@code some.class.Name.member...}.
*/
static Stream getStaticMemberCompletions(Class> cl, String name) {
final TreeSet names = new TreeSet<>();
AtomExprParser.getMemberNames(cl, names, true);
names.add("class");
return ParseUtil.complete(names, name);
}
/**
* Get all public instance member name completions, including bean property names, for {@code some.class.Name.member...}.
*/
static Stream getInstanceMemberCompletions(Class> cl, String name) {
final TreeSet names = new TreeSet<>();
AtomExprParser.getMemberNames(cl, names, false);
AtomExprParser.getBeanPropertyNames(cl, names);
if (cl.isArray())
names.add("length");
return ParseUtil.complete(names, name);
}
private static Iterable getMemberNames(Class> cl, TreeSet names, boolean isStatic) {
// Add public and private fields
int mask = Modifier.STATIC;
int flags = isStatic ? Modifier.STATIC : 0;
for (Field field : cl.getDeclaredFields()) {
if ((field.getModifiers() & mask) != flags)
continue;
names.add(field.getName());
}
// Add public methods only
mask |= Modifier.PUBLIC;
flags |= Modifier.PUBLIC;
for (Method method : cl.getDeclaredMethods()) {
if ((method.getModifiers() & mask) != flags)
continue;
names.add(method.getName() + "(");
}
// Recurse on supertypes
if (cl.getSuperclass() != null)
AtomExprParser.getMemberNames(cl.getSuperclass(), names, isStatic);
for (Class> iface : cl.getInterfaces())
AtomExprParser.getMemberNames(iface, names, isStatic);
// Done
return names;
}
// Parse method parameters; we assume opening `(' has just been parsed
static List parseParams(ParseSession session, ParseContext ctx, boolean complete) {
final ArrayList params = new ArrayList<>();
final SpaceParser spaceParser = new SpaceParser();
spaceParser.parse(ctx, complete);
if (ctx.tryLiteral(")"))
return params;
while (true) {
params.add(ExprParser.INSTANCE.parse(session, ctx, complete));
spaceParser.parse(ctx, complete);
if (ctx.tryLiteral(")"))
break;
if (!ctx.tryLiteral(","))
throw new ParseException(ctx, "expected `,'").addCompletion(", ");
spaceParser.parse(ctx, complete);
}
return params;
}
// Get bean property names
static void getBeanPropertyNames(Class> cl, TreeSet names) {
final BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(cl);
} catch (IntrospectionException e) {
return;
}
for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
if (propertyDescriptor instanceof IndexedPropertyDescriptor)
continue;
if (propertyDescriptor.getReadMethod() == null)
continue;
names.add(propertyDescriptor.getName());
}
}
// ClassNameCompletion
private static class ClassNameCompletion {
private final String prefix;
private final boolean hasDot;
ClassNameCompletion(String prefix) {
this.prefix = prefix;
this.hasDot = prefix.indexOf('.') != -1;
}
public Stream findCompletions(ParseSession session) {
// Create search patterns
final HashSet patterns = new HashSet<>();
if (!this.hasDot) {
for (String mport : session.getImports()) {
mport = mport.replace('.', '/');
if (mport.length() > 1 && mport.charAt(mport.length() - 2) == '/' && mport.charAt(mport.length() - 1) == '*') {
patterns.add(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ mport.substring(0, mport.length() - 1) + this.prefix + "*.class");
} else if (mport.lastIndexOf('/') != -1 && mport.substring(mport.lastIndexOf('/') + 1).startsWith(this.prefix))
patterns.add(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + mport + ".class");
}
} else {
String path = this.prefix.replace('.', '/');
patterns.add(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + path + "*.class");
}
// Search for matching class resources
final HashSet classNames = new HashSet<>();
for (String pattern : patterns) {
final Resource[] resources;
try {
resources = new PathMatchingResourcePatternResolver().getResources(pattern);
} catch (IOException e) {
continue;
}
for (Resource resource : resources) {
String name;
if (this.hasDot) {
final String uri;
try {
uri = resource.getURI().toString();
} catch (IOException e) {
continue;
}
final int npos = uri.lastIndexOf(this.prefix.replace('.', '/'));
name = uri.substring(npos).replace('/', '.');
} else {
name = resource.getFilename();
}
if (name.endsWith(".class")) // should always be the case
name = name.substring(0, name.length() - 6);
final int dot = name.indexOf('.', this.prefix.length());
if (dot != -1)
name = name.substring(0, dot); // stop at '.' separators
final int dollar = name.indexOf('$', this.prefix.length());
if (dollar != -1)
name = name.substring(0, dollar); // stop at '$' separators
classNames.add(name);
}
}
return ParseUtil.complete(classNames, this.prefix);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy