PermissionsParser.java
/*
* 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 org.apache.commons.jexl3.internal.introspection;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A crude parser to configure permissions akin to NoJexl annotations.
* The syntax recognizes 2 types of permissions:
* <ul>
* <li>restricting access to packages, classes (and inner classes), methods and fields</li>
* <li>allowing access to a wildcard restricted set of packages</li>
* </ul>
* <p>
* Example:
* </p>
* <pre>
* my.allowed.packages.*
* another.allowed.package.*
* # nojexl like restrictions
* my.package {
* class0 {...
* class1 {...}
* class2 {
* ...
* class3 {}
* }
* # and eol comment
* class0(); # constructors
* method(); # method is not allowed
* field; # field
* } # end class0
* +class1 {
* method(); // only allowed method of class1
* }
* } # end package my.package
* </pre>
*/
public class PermissionsParser {
/** The source. */
private String src;
/** The source size. */
private int size;
/** The @NoJexl execution-time map. */
private Map<String, Permissions.NoJexlPackage> packages;
/** The set of wildcard imports. */
private Set<String> wildcards;
/**
* Basic ctor.
*/
public PermissionsParser() {
// nothing besides default member initialization
}
/**
* Clears this parser internals.
*/
private void clear() {
src = null; size = 0; packages = null; wildcards = null;
}
/**
* Parses permissions from a source.
* @param wildcards the set of allowed packages
* @param packages the map of restricted elements
* @param srcs the sources
* @return the permissions map
*/
synchronized Permissions parse(final Set<String> wildcards, final Map<String, Permissions.NoJexlPackage> packages,
final String... srcs) {
try {
if (srcs == null || srcs.length == 0) {
return Permissions.UNRESTRICTED;
}
this.packages = packages;
this.wildcards = wildcards;
for (final String source : srcs) {
this.src = source;
this.size = source.length();
readPackages();
}
return new Permissions(wildcards, packages);
} finally {
clear();
}
}
/**
* Parses permissions from a source.
* @param srcs the sources
* @return the permissions map
*/
public Permissions parse(final String... srcs) {
return parse(new LinkedHashSet<>(), new ConcurrentHashMap<>(), srcs);
}
/**
* Reads a class permission.
* @param njpackage the owning package
* @param nojexl whether the restriction is explicitly denying (true) or allowing (false) members
* @param outer the outer class (if any)
* @param inner the inner class name (if any)
* @param offset the initial parsing position in the source
* @return the new parsing position
*/
private int readClass(final Permissions.NoJexlPackage njpackage, final boolean nojexl, final String outer, final String inner, final int offset) {
final StringBuilder temp = new StringBuilder();
Permissions.NoJexlClass njclass = null;
String njname = null;
String identifier = inner;
boolean deny = nojexl;
int i = offset;
int j = -1;
boolean isMethod = false;
while(i < size) {
final char c = src.charAt(i);
// if no parsing progress can be made, we are in error
if (j >= i) {
throw new IllegalStateException(unexpected(c, i));
}
j = i;
// get rid of space
if (Character.isWhitespace(c)) {
i = readSpaces(i + 1);
continue;
}
// eol comment
if (c == '#') {
i = readEol(i + 1);
continue;
}
// end of class ?
if (njclass != null && c == '}') {
i += 1;
break;
}
// read an identifier, the class name
if (identifier == null) {
// negative or positive set ?
if (c == '-') {
i += 1;
} else if (c == '+') {
deny = false;
i += 1;
}
final int next = readIdentifier(temp, i);
if (i != next) {
identifier = temp.toString();
temp.setLength(0);
i = next;
continue;
}
}
// parse a class:
if (njclass == null) {
// we must have read the class ('identifier {'...)
if (identifier == null || c != '{') {
throw new IllegalStateException(unexpected(c, i));
}
// if we have a class, it has a name
njclass = deny ? new Permissions.NoJexlClass() : new Permissions.JexlClass();
njname = outer != null ? outer + "$" + identifier : identifier;
njpackage.addNoJexl(njname, njclass);
identifier = null;
} else if (identifier != null) {
// class member mode
if (c == '{') {
// inner class
i = readClass(njpackage, deny, njname, identifier, i - 1);
identifier = null;
continue;
}
if (c == ';') {
// field or method?
if (isMethod) {
njclass.methodNames.add(identifier);
isMethod = false;
} else {
njclass.fieldNames.add(identifier);
}
identifier = null;
} else if (c == '(' && !isMethod) {
// method; only one opening parenthesis allowed
isMethod = true;
} else if (c != ')' || src.charAt(i - 1) != '(') {
// closing parenthesis following opening one was expected
throw new IllegalStateException(unexpected(c, i));
}
}
i += 1;
}
// empty class means allow or deny all
if (njname != null && njclass.isEmpty()) {
njpackage.addNoJexl(njname, njclass instanceof Permissions.JexlClass
? Permissions.JEXL_CLASS
: Permissions.NOJEXL_CLASS);
}
return i;
}
/**
* Reads a comment till end-of-line.
* @param offset initial position
* @return position after comment
*/
private int readEol(final int offset) {
int i = offset;
while (i < size) {
final char c = src.charAt(i);
if (c == '\n') {
break;
}
i += 1;
}
return i;
}
/**
* Reads an identifier (optionally dot-separated).
* @param id the builder to fill the identifier character with
* @param offset the initial reading position
* @return the position after the identifier
*/
private int readIdentifier(final StringBuilder id, final int offset) {
return readIdentifier(id, offset, false, false);
}
/**
* Reads an identifier (optionally dot-separated).
* @param id the builder to fill the identifier character with
* @param offset the initial reading position
* @param dot whether dots (.) are allowed
* @param star whether stars (*) are allowed
* @return the position after the identifier
*/
private int readIdentifier(final StringBuilder id, final int offset, final boolean dot, final boolean star) {
int begin = -1;
boolean starf = star;
int i = offset;
char c = 0;
while (i < size) {
c = src.charAt(i);
// accumulate identifier characters
if (Character.isJavaIdentifierStart(c) && begin < 0) {
begin = i;
id.append(c);
} else if (Character.isJavaIdentifierPart(c) && begin >= 0) {
id.append(c);
} else if (dot && c == '.') {
if (src.charAt(i - 1) == '.') {
throw new IllegalStateException(unexpected(c, i));
}
id.append('.');
begin = -1;
} else if (starf && c == '*') {
id.append('*');
starf = false; // only one star
} else {
break;
}
i += 1;
}
// cant end with a dot
if (dot && c == '.') {
throw new IllegalStateException(unexpected(c, i));
}
return i;
}
/**
* Reads a package permission.
*/
private void readPackages() {
final StringBuilder temp = new StringBuilder();
Permissions.NoJexlPackage njpackage = null;
int i = 0;
int j = -1;
String pname = null;
while (i < size) {
final char c = src.charAt(i);
// if no parsing progress can be made, we are in error
if (j >= i) {
throw new IllegalStateException(unexpected(c, i));
}
j = i;
// get rid of space
if (Character.isWhitespace(c)) {
i = readSpaces(i + 1);
continue;
}
// eol comment
if (c == '#') {
i = readEol(i + 1);
continue;
}
// read the package qualified name
if (pname == null) {
final int next = readIdentifier(temp, i, true, true);
if (i != next) {
pname = temp.toString();
temp.setLength(0);
i = next;
// consume it if it is a wildcard declaration
if (pname.endsWith(".*")) {
wildcards.add(pname);
pname = null;
}
continue;
}
}
// package mode
if (njpackage == null) {
if (c == '{') {
njpackage = packages.compute(pname,
(n, p) -> new Permissions.NoJexlPackage(p == null? null : p.nojexl)
);
i += 1;
}
} else if (c == '}') {
// empty means whole package
if (njpackage.isEmpty()) {
packages.put(pname, Permissions.NOJEXL_PACKAGE);
}
njpackage = null; // can restart anew
pname = null;
i += 1;
} else {
i = readClass(njpackage, true,null, null, i);
}
}
}
/**
* Reads spaces.
* @param offset initial position
* @return position after spaces
*/
private int readSpaces(final int offset) {
int i = offset;
while (i < size) {
final char c = src.charAt(i);
if (!Character.isWhitespace(c)) {
break;
}
i += 1;
}
return offset;
}
/**
* Compose a parsing error message.
* @param c the offending character
* @param i the offset position
* @return the error message
*/
private String unexpected(final char c, final int i) {
return "unexpected '" + c + "'" + "@" + i;
}
}