http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java b/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java new file mode 100644 index 0000000..1bb302b --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java @@ -0,0 +1,1189 @@ +/* + * 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.codehaus.groovy.control.customizers; + +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.GroovyCodeVisitor; +import org.codehaus.groovy.ast.ImportNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.ArrayExpression; +import org.codehaus.groovy.ast.expr.AttributeExpression; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.BitwiseNegationExpression; +import org.codehaus.groovy.ast.expr.BooleanExpression; +import org.codehaus.groovy.ast.expr.CastExpression; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.ClosureListExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.ConstructorCallExpression; +import org.codehaus.groovy.ast.expr.DeclarationExpression; +import org.codehaus.groovy.ast.expr.ElvisOperatorExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.FieldExpression; +import org.codehaus.groovy.ast.expr.GStringExpression; +import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.ast.expr.MapEntryExpression; +import org.codehaus.groovy.ast.expr.MapExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.MethodPointerExpression; +import org.codehaus.groovy.ast.expr.NotExpression; +import org.codehaus.groovy.ast.expr.PostfixExpression; +import org.codehaus.groovy.ast.expr.PrefixExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.expr.RangeExpression; +import org.codehaus.groovy.ast.expr.SpreadExpression; +import org.codehaus.groovy.ast.expr.SpreadMapExpression; +import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; +import org.codehaus.groovy.ast.expr.TernaryExpression; +import org.codehaus.groovy.ast.expr.TupleExpression; +import org.codehaus.groovy.ast.expr.UnaryMinusExpression; +import org.codehaus.groovy.ast.expr.UnaryPlusExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.AssertStatement; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.BreakStatement; +import org.codehaus.groovy.ast.stmt.CaseStatement; +import org.codehaus.groovy.ast.stmt.CatchStatement; +import org.codehaus.groovy.ast.stmt.ContinueStatement; +import org.codehaus.groovy.ast.stmt.DoWhileStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.ForStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.ast.stmt.SwitchStatement; +import org.codehaus.groovy.ast.stmt.SynchronizedStatement; +import org.codehaus.groovy.ast.stmt.ThrowStatement; +import org.codehaus.groovy.ast.stmt.TryCatchStatement; +import org.codehaus.groovy.ast.stmt.WhileStatement; +import org.codehaus.groovy.classgen.BytecodeExpression; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.syntax.Token; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * This customizer allows securing source code by controlling what code constructs are allowed. For example, if you only + * want to allow arithmetic operations in a groovy shell, you can configure this customizer to restrict package imports, + * method calls and so on. + * <p> + * Most of the security customization options found in this class work with either blacklist or whitelist. This means that, for a + * single option, you can set a whitelist OR a blacklist, but not both. You can mix whitelist/blacklist strategies for + * different options. For example, you can have import whitelist and tokens blacklist. + * <p> + * The recommended way of securing shells is to use whitelists because it is guaranteed that future features of the + * Groovy language won't be allowed by defaut. Using blacklists, you can limit the features of the languages by opting + * out, but new language features would require you to update your configuration. + * <p> + * If you set neither a whitelist nor a blacklist, then everything is authorized. + * <p> + * Combinations of import and star imports constraints are authorized as long as you use the same type of list for both. + * For example, you may use an import whitelist and a star import whitelist together, but you cannot use an import white + * list with a star import blacklist. static imports are handled separately, meaning that blacklisting an import <b> + * does not</b> prevent from using a static import. + * <p> + * Eventually, if the features provided here are not sufficient, you may implement custom AST filtering handlers, either + * implementing the {@link StatementChecker} interface or {@link ExpressionChecker} interface then register your + * handlers thanks to the {@link #addExpressionCheckers(org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker...)} + * and {@link #addStatementCheckers(org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker...)} + * methods. + * <p> + * Here is an example of usage. We will create a groovy classloader which only supports arithmetic operations and imports + * the java.lang.Math classes by default. + * + * <pre> + * final ImportCustomizer imports = new ImportCustomizer().addStaticStars('java.lang.Math') // add static import of java.lang.Math + * final SecureASTCustomizer secure = new SecureASTCustomizer() + * secure.with { + * closuresAllowed = false + * methodDefinitionAllowed = false + * + * importsWhitelist = [] + * staticImportsWhitelist = [] + * staticStarImportsWhitelist = ['java.lang.Math'] // only java.lang.Math is allowed + * + * tokensWhitelist = [ + * PLUS, + * MINUS, + * MULTIPLY, + * DIVIDE, + * MOD, + * POWER, + * PLUS_PLUS, + * MINUS_MINUS, + * COMPARE_EQUAL, + * COMPARE_NOT_EQUAL, + * COMPARE_LESS_THAN, + * COMPARE_LESS_THAN_EQUAL, + * COMPARE_GREATER_THAN, + * COMPARE_GREATER_THAN_EQUAL, + * ].asImmutable() + * + * constantTypesClassesWhiteList = [ + * Integer, + * Float, + * Long, + * Double, + * BigDecimal, + * Integer.TYPE, + * Long.TYPE, + * Float.TYPE, + * Double.TYPE + * ].asImmutable() + * + * receiversClassesWhiteList = [ + * Math, + * Integer, + * Float, + * Double, + * Long, + * BigDecimal + * ].asImmutable() + * } + * CompilerConfiguration config = new CompilerConfiguration() + * config.addCompilationCustomizers(imports, secure) + * GroovyClassLoader loader = new GroovyClassLoader(this.class.classLoader, config) + * </pre> + * + * @author Cedric Champeau + * @author Guillaume Laforge + * @author Hamlet D'Arcy + * @since 1.8.0 + */ +public class SecureASTCustomizer extends CompilationCustomizer { + + private boolean isPackageAllowed = true; + private boolean isMethodDefinitionAllowed = true; + private boolean isClosuresAllowed = true; + + // imports + private List<String> importsWhitelist; + private List<String> importsBlacklist; + + // static imports + private List<String> staticImportsWhitelist; + private List<String> staticImportsBlacklist; + + // star imports + private List<String> starImportsWhitelist; + private List<String> starImportsBlacklist; + + // static star imports + private List<String> staticStarImportsWhitelist; + private List<String> staticStarImportsBlacklist; + + + // indirect import checks + // if set to true, then security rules on imports will also be applied on classnodes. + // Direct instantiation of classes without imports will therefore also fail if this option is enabled + private boolean isIndirectImportCheckEnabled; + + // statements + private List<Class<? extends Statement>> statementsWhitelist; + private List<Class<? extends Statement>> statementsBlacklist; + private final List<StatementChecker> statementCheckers = new LinkedList<StatementChecker>(); + + // expressions + private List<Class<? extends Expression>> expressionsWhitelist; + private List<Class<? extends Expression>> expressionsBlacklist; + private final List<ExpressionChecker> expressionCheckers = new LinkedList<ExpressionChecker>(); + + // tokens from Types + private List<Integer> tokensWhitelist; + private List<Integer> tokensBlacklist; + + // constant types + private List<String> constantTypesWhiteList; + private List<String> constantTypesBlackList; + + // receivers + private List<String> receiversWhiteList; + private List<String> receiversBlackList; + + public SecureASTCustomizer() { + super(CompilePhase.CANONICALIZATION); + } + + public boolean isMethodDefinitionAllowed() { + return isMethodDefinitionAllowed; + } + + public void setMethodDefinitionAllowed(final boolean methodDefinitionAllowed) { + isMethodDefinitionAllowed = methodDefinitionAllowed; + } + + public boolean isPackageAllowed() { + return isPackageAllowed; + } + + public boolean isClosuresAllowed() { + return isClosuresAllowed; + } + + public void setClosuresAllowed(final boolean closuresAllowed) { + isClosuresAllowed = closuresAllowed; + } + + public void setPackageAllowed(final boolean packageAllowed) { + isPackageAllowed = packageAllowed; + } + + public List<String> getImportsBlacklist() { + return importsBlacklist; + } + + public void setImportsBlacklist(final List<String> importsBlacklist) { + if (importsWhitelist != null || starImportsWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.importsBlacklist = importsBlacklist; + } + + public List<String> getImportsWhitelist() { + return importsWhitelist; + } + + public void setImportsWhitelist(final List<String> importsWhitelist) { + if (importsBlacklist != null || starImportsBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.importsWhitelist = importsWhitelist; + } + + public List<String> getStarImportsBlacklist() { + return starImportsBlacklist; + } + + public void setStarImportsBlacklist(final List<String> starImportsBlacklist) { + if (importsWhitelist != null || starImportsWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.starImportsBlacklist = normalizeStarImports(starImportsBlacklist); + if (this.importsBlacklist == null) importsBlacklist = Collections.emptyList(); + } + + public List<String> getStarImportsWhitelist() { + return starImportsWhitelist; + } + + public void setStarImportsWhitelist(final List<String> starImportsWhitelist) { + if (importsBlacklist != null || starImportsBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.starImportsWhitelist = normalizeStarImports(starImportsWhitelist); + if (this.importsWhitelist == null) importsWhitelist = Collections.emptyList(); + } + + /** + * Ensures that every star import ends with .* as this is the expected syntax in import checks. + */ + private static List<String> normalizeStarImports(List<String> starImports) { + List<String> result = new ArrayList<String>(starImports.size()); + for (String starImport : starImports) { + if (starImport.endsWith(".*")) { + result.add(starImport); + } else if (starImport.endsWith(".")) { + result.add(starImport + "*"); + } else { + result.add(starImport + ".*"); + } + } + return Collections.unmodifiableList(result); + } + + public List<String> getStaticImportsBlacklist() { + return staticImportsBlacklist; + } + + public void setStaticImportsBlacklist(final List<String> staticImportsBlacklist) { + if (staticImportsWhitelist != null || staticStarImportsWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.staticImportsBlacklist = staticImportsBlacklist; + } + + public List<String> getStaticImportsWhitelist() { + return staticImportsWhitelist; + } + + public void setStaticImportsWhitelist(final List<String> staticImportsWhitelist) { + if (staticImportsBlacklist != null || staticStarImportsBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.staticImportsWhitelist = staticImportsWhitelist; + } + + public List<String> getStaticStarImportsBlacklist() { + return staticStarImportsBlacklist; + } + + public void setStaticStarImportsBlacklist(final List<String> staticStarImportsBlacklist) { + if (staticImportsWhitelist != null || staticStarImportsWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.staticStarImportsBlacklist = normalizeStarImports(staticStarImportsBlacklist); + if (this.staticImportsBlacklist == null) this.staticImportsBlacklist = Collections.emptyList(); + } + + public List<String> getStaticStarImportsWhitelist() { + return staticStarImportsWhitelist; + } + + public void setStaticStarImportsWhitelist(final List<String> staticStarImportsWhitelist) { + if (staticImportsBlacklist != null || staticStarImportsBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.staticStarImportsWhitelist = normalizeStarImports(staticStarImportsWhitelist); + if (this.staticImportsWhitelist == null) this.staticImportsWhitelist = Collections.emptyList(); + } + + public List<Class<? extends Expression>> getExpressionsBlacklist() { + return expressionsBlacklist; + } + + public void setExpressionsBlacklist(final List<Class<? extends Expression>> expressionsBlacklist) { + if (expressionsWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.expressionsBlacklist = expressionsBlacklist; + } + + public List<Class<? extends Expression>> getExpressionsWhitelist() { + return expressionsWhitelist; + } + + public void setExpressionsWhitelist(final List<Class<? extends Expression>> expressionsWhitelist) { + if (expressionsBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.expressionsWhitelist = expressionsWhitelist; + } + + public List<Class<? extends Statement>> getStatementsBlacklist() { + return statementsBlacklist; + } + + public void setStatementsBlacklist(final List<Class<? extends Statement>> statementsBlacklist) { + if (statementsWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.statementsBlacklist = statementsBlacklist; + } + + public List<Class<? extends Statement>> getStatementsWhitelist() { + return statementsWhitelist; + } + + public void setStatementsWhitelist(final List<Class<? extends Statement>> statementsWhitelist) { + if (statementsBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.statementsWhitelist = statementsWhitelist; + } + + public List<Integer> getTokensBlacklist() { + return tokensBlacklist; + } + + public boolean isIndirectImportCheckEnabled() { + return isIndirectImportCheckEnabled; + } + + /** + * Set this option to true if you want your import rules to be checked against every class node. This means that if + * someone uses a fully qualified class name, then it will also be checked against the import rules, preventing, for + * example, instantiation of classes without imports thanks to FQCN. + * + * @param indirectImportCheckEnabled set to true to enable indirect checks + */ + public void setIndirectImportCheckEnabled(final boolean indirectImportCheckEnabled) { + isIndirectImportCheckEnabled = indirectImportCheckEnabled; + } + + /** + * Sets the list of tokens which are blacklisted. + * + * @param tokensBlacklist the tokens. The values of the tokens must be those of {@link org.codehaus.groovy.syntax.Types} + */ + public void setTokensBlacklist(final List<Integer> tokensBlacklist) { + if (tokensWhitelist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.tokensBlacklist = tokensBlacklist; + } + + public List<Integer> getTokensWhitelist() { + return tokensWhitelist; + } + + /** + * Sets the list of tokens which are whitelisted. + * + * @param tokensWhitelist the tokens. The values of the tokens must be those of {@link org.codehaus.groovy.syntax.Types} + */ + public void setTokensWhitelist(final List<Integer> tokensWhitelist) { + if (tokensBlacklist != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.tokensWhitelist = tokensWhitelist; + } + + public void addStatementCheckers(StatementChecker... checkers) { + statementCheckers.addAll(Arrays.asList(checkers)); + } + + public void addExpressionCheckers(ExpressionChecker... checkers) { + expressionCheckers.addAll(Arrays.asList(checkers)); + } + + public List<String> getConstantTypesBlackList() { + return constantTypesBlackList; + } + + public void setConstantTypesBlackList(final List<String> constantTypesBlackList) { + if (constantTypesWhiteList != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.constantTypesBlackList = constantTypesBlackList; + } + + public List<String> getConstantTypesWhiteList() { + return constantTypesWhiteList; + } + + public void setConstantTypesWhiteList(final List<String> constantTypesWhiteList) { + if (constantTypesBlackList != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.constantTypesWhiteList = constantTypesWhiteList; + } + + /** + * An alternative way of setting constant types. + * + * @param constantTypesWhiteList a list of classes. + */ + public void setConstantTypesClassesWhiteList(final List<Class> constantTypesWhiteList) { + List<String> values = new LinkedList<String>(); + for (Class aClass : constantTypesWhiteList) { + values.add(aClass.getName()); + } + setConstantTypesWhiteList(values); + } + + /** + * An alternative way of setting constant types. + * + * @param constantTypesBlackList a list of classes. + */ + public void setConstantTypesClassesBlackList(final List<Class> constantTypesBlackList) { + List<String> values = new LinkedList<String>(); + for (Class aClass : constantTypesBlackList) { + values.add(aClass.getName()); + } + setConstantTypesBlackList(values); + } + + public List<String> getReceiversBlackList() { + return receiversBlackList; + } + + /** + * Sets the list of classes which deny method calls. + * + * Please note that since Groovy is a dynamic language, and + * this class performs a static type check, it will be reletively + * simple to bypass any blacklist unless the receivers blacklist contains, at + * a minimum, Object, Script, GroovyShell, and Eval. Additionally, + * it is necessary to also blacklist MethodPointerExpression in the + * expressions blacklist for the receivers blacklist to function + * as a security check. + * + * @param receiversBlackList the list of refused classes, as fully qualified names + */ + public void setReceiversBlackList(final List<String> receiversBlackList) { + if (receiversWhiteList != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.receiversBlackList = receiversBlackList; + } + + /** + * An alternative way of setting {@link #setReceiversBlackList(java.util.List) receiver classes}. + * + * @param receiversBlacklist a list of classes. + */ + public void setReceiversClassesBlackList(final List<Class> receiversBlacklist) { + List<String> values = new LinkedList<String>(); + for (Class aClass : receiversBlacklist) { + values.add(aClass.getName()); + } + setReceiversBlackList(values); + } + + public List<String> getReceiversWhiteList() { + return receiversWhiteList; + } + + /** + * Sets the list of classes which may accept method calls. + * + * @param receiversWhiteList the list of accepted classes, as fully qualified names + */ + public void setReceiversWhiteList(final List<String> receiversWhiteList) { + if (receiversBlackList != null) { + throw new IllegalArgumentException("You are not allowed to set both whitelist and blacklist"); + } + this.receiversWhiteList = receiversWhiteList; + } + + /** + * An alternative way of setting {@link #setReceiversWhiteList(java.util.List) receiver classes}. + * + * @param receiversWhitelist a list of classes. + */ + public void setReceiversClassesWhiteList(final List<Class> receiversWhitelist) { + List<String> values = new LinkedList<String>(); + for (Class aClass : receiversWhitelist) { + values.add(aClass.getName()); + } + setReceiversWhiteList(values); + } + + @Override + public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException { + final ModuleNode ast = source.getAST(); + if (!isPackageAllowed && ast.getPackage() != null) { + throw new SecurityException("Package definitions are not allowed"); + } + checkMethodDefinitionAllowed(classNode); + + // verify imports + if (importsBlacklist != null || importsWhitelist != null || starImportsBlacklist != null || starImportsWhitelist != null) { + for (ImportNode importNode : ast.getImports()) { + final String className = importNode.getClassName(); + assertImportIsAllowed(className); + } + for (ImportNode importNode : ast.getStarImports()) { + final String className = importNode.getPackageName(); + assertStarImportIsAllowed(className + "*"); + } + } + + // verify static imports + if (staticImportsBlacklist != null || staticImportsWhitelist != null || staticStarImportsBlacklist != null || staticStarImportsWhitelist != null) { + for (Map.Entry<String, ImportNode> entry : ast.getStaticImports().entrySet()) { + final String className = entry.getValue().getClassName(); + assertStaticImportIsAllowed(entry.getKey(), className); + } + for (Map.Entry<String, ImportNode> entry : ast.getStaticStarImports().entrySet()) { + final String className = entry.getValue().getClassName(); + assertStaticImportIsAllowed(entry.getKey(), className); + } + } + + final SecuringCodeVisitor visitor = new SecuringCodeVisitor(); + ast.getStatementBlock().visit(visitor); + for (ClassNode clNode : ast.getClasses()) { + if (clNode!=classNode) { + checkMethodDefinitionAllowed(clNode); + for (MethodNode methodNode : clNode.getMethods()) { + if (!methodNode.isSynthetic() && methodNode.getCode() != null) { + methodNode.getCode().visit(visitor); + } + } + } + } + + List<MethodNode> methods = filterMethods(classNode); + if (isMethodDefinitionAllowed) { + for (MethodNode method : methods) { + if (method.getDeclaringClass()==classNode && method.getCode() != null) method.getCode().visit(visitor); + } + } + } + + private void checkMethodDefinitionAllowed(ClassNode owner) { + if (isMethodDefinitionAllowed) return; + List<MethodNode> methods = filterMethods(owner); + if (!methods.isEmpty()) throw new SecurityException("Method definitions are not allowed"); + } + + private static List<MethodNode> filterMethods(ClassNode owner) { + List<MethodNode> result = new LinkedList<MethodNode>(); + List<MethodNode> methods = owner.getMethods(); + for (MethodNode method : methods) { + if (method.getDeclaringClass() == owner && !method.isSynthetic()) { + if ("main".equals(method.getName()) || "run".equals(method.getName()) && owner.isScriptBody()) continue; + result.add(method); + } + } + return result; + } + + private void assertStarImportIsAllowed(final String packageName) { + if (starImportsWhitelist != null && !starImportsWhitelist.contains(packageName)) { + throw new SecurityException("Importing [" + packageName + "] is not allowed"); + } + if (starImportsBlacklist != null && starImportsBlacklist.contains(packageName)) { + throw new SecurityException("Importing [" + packageName + "] is not allowed"); + } + } + + private void assertImportIsAllowed(final String className) { + if (importsWhitelist != null && !importsWhitelist.contains(className)) { + if (starImportsWhitelist != null) { + // we should now check if the import is in the star imports + ClassNode node = ClassHelper.make(className); + final String packageName = node.getPackageName(); + if (!starImportsWhitelist.contains(packageName + ".*")) { + throw new SecurityException("Importing [" + className + "] is not allowed"); + } + } else { + throw new SecurityException("Importing [" + className + "] is not allowed"); + } + } + if (importsBlacklist != null && importsBlacklist.contains(className)) { + throw new SecurityException("Importing [" + className + "] is not allowed"); + } + // check that there's no star import blacklist + if (starImportsBlacklist != null) { + ClassNode node = ClassHelper.make(className); + final String packageName = node.getPackageName(); + if (starImportsBlacklist.contains(packageName + ".*")) { + throw new SecurityException("Importing [" + className + "] is not allowed"); + } + } + } + + private void assertStaticImportIsAllowed(final String member, final String className) { + final String fqn = member.equals(className) ? member : className + "." + member; + if (staticImportsWhitelist != null && !staticImportsWhitelist.contains(fqn)) { + if (staticStarImportsWhitelist != null) { + // we should now check if the import is in the star imports + if (!staticStarImportsWhitelist.contains(className + ".*")) { + throw new SecurityException("Importing [" + fqn + "] is not allowed"); + } + } else { + throw new SecurityException("Importing [" + fqn + "] is not allowed"); + } + } + if (staticImportsBlacklist != null && staticImportsBlacklist.contains(fqn)) { + throw new SecurityException("Importing [" + fqn + "] is not allowed"); + } + // check that there's no star import blacklist + if (staticStarImportsBlacklist != null) { + if (staticStarImportsBlacklist.contains(className + ".*")) { + throw new SecurityException("Importing [" + fqn + "] is not allowed"); + } + } + } + + /** + * This visitor directly implements the {@link GroovyCodeVisitor} interface instead of using the {@link + * CodeVisitorSupport} class to make sure that future features of the language gets managed by this visitor. Thus, + * adding a new feature would result in a compilation error if this visitor is not updated. + */ + private class SecuringCodeVisitor implements GroovyCodeVisitor { + + /** + * Checks that a given statement is either in the whitelist or not in the blacklist. + * + * @param statement the statement to be checked + * @throws SecurityException if usage of this statement class is forbidden + */ + private void assertStatementAuthorized(final Statement statement) throws SecurityException { + final Class<? extends Statement> clazz = statement.getClass(); + if (statementsBlacklist != null && statementsBlacklist.contains(clazz)) { + throw new SecurityException(clazz.getSimpleName() + "s are not allowed"); + } else if (statementsWhitelist != null && !statementsWhitelist.contains(clazz)) { + throw new SecurityException(clazz.getSimpleName() + "s are not allowed"); + } + for (StatementChecker statementChecker : statementCheckers) { + if (!statementChecker.isAuthorized(statement)) { + throw new SecurityException("Statement [" + clazz.getSimpleName() + "] is not allowed"); + } + } + } + + /** + * Checks that a given expression is either in the whitelist or not in the blacklist. + * + * @param expression the expression to be checked + * @throws SecurityException if usage of this expression class is forbidden + */ + private void assertExpressionAuthorized(final Expression expression) throws SecurityException { + final Class<? extends Expression> clazz = expression.getClass(); + if (expressionsBlacklist != null && expressionsBlacklist.contains(clazz)) { + throw new SecurityException(clazz.getSimpleName() + "s are not allowed: " + expression.getText()); + } else if (expressionsWhitelist != null && !expressionsWhitelist.contains(clazz)) { + throw new SecurityException(clazz.getSimpleName() + "s are not allowed: " + expression.getText()); + } + for (ExpressionChecker expressionChecker : expressionCheckers) { + if (!expressionChecker.isAuthorized(expression)) { + throw new SecurityException("Expression [" + clazz.getSimpleName() + "] is not allowed: " + expression.getText()); + } + } + if (isIndirectImportCheckEnabled) { + try { + if (expression instanceof ConstructorCallExpression) { + assertImportIsAllowed(expression.getType().getName()); + } else if (expression instanceof MethodCallExpression) { + MethodCallExpression expr = (MethodCallExpression) expression; + ClassNode objectExpressionType = expr.getObjectExpression().getType(); + final String typename = getExpressionType(objectExpressionType).getName(); + assertImportIsAllowed(typename); + assertStaticImportIsAllowed(expr.getMethodAsString(), typename); + } else if (expression instanceof StaticMethodCallExpression) { + StaticMethodCallExpression expr = (StaticMethodCallExpression) expression; + final String typename = expr.getOwnerType().getName(); + assertImportIsAllowed(typename); + assertStaticImportIsAllowed(expr.getMethod(), typename); + } else if (expression instanceof MethodPointerExpression) { + MethodPointerExpression expr = (MethodPointerExpression) expression; + final String typename = expr.getType().getName(); + assertImportIsAllowed(typename); + assertStaticImportIsAllowed(expr.getText(), typename); + } + } catch (SecurityException e) { + throw new SecurityException("Indirect import checks prevents usage of expression", e); + } + } + } + + private ClassNode getExpressionType(ClassNode objectExpressionType) { + return objectExpressionType.isArray() ? getExpressionType(objectExpressionType.getComponentType()) : objectExpressionType; + } + + /** + * Checks that a given token is either in the whitelist or not in the blacklist. + * + * @param token the token to be checked + * @throws SecurityException if usage of this token is forbidden + */ + private void assertTokenAuthorized(final Token token) throws SecurityException { + final int value = token.getType(); + if (tokensBlacklist != null && tokensBlacklist.contains(value)) { + throw new SecurityException("Token " + token + " is not allowed"); + } else if (tokensWhitelist != null && !tokensWhitelist.contains(value)) { + throw new SecurityException("Token " + token + " is not allowed"); + } + } + + public void visitBlockStatement(final BlockStatement block) { + assertStatementAuthorized(block); + for (Statement statement : block.getStatements()) { + statement.visit(this); + } + } + + + public void visitForLoop(final ForStatement forLoop) { + assertStatementAuthorized(forLoop); + forLoop.getCollectionExpression().visit(this); + forLoop.getLoopBlock().visit(this); + } + + public void visitWhileLoop(final WhileStatement loop) { + assertStatementAuthorized(loop); + loop.getBooleanExpression().visit(this); + loop.getLoopBlock().visit(this); + } + + public void visitDoWhileLoop(final DoWhileStatement loop) { + assertStatementAuthorized(loop); + loop.getBooleanExpression().visit(this); + loop.getLoopBlock().visit(this); + } + + public void visitIfElse(final IfStatement ifElse) { + assertStatementAuthorized(ifElse); + ifElse.getBooleanExpression().visit(this); + ifElse.getIfBlock().visit(this); + + Statement elseBlock = ifElse.getElseBlock(); + if (elseBlock instanceof EmptyStatement) { + // dispatching to EmptyStatement will not call back visitor, + // must call our visitEmptyStatement explicitly + visitEmptyStatement((EmptyStatement) elseBlock); + } else { + elseBlock.visit(this); + } + } + + public void visitExpressionStatement(final ExpressionStatement statement) { + assertStatementAuthorized(statement); + statement.getExpression().visit(this); + } + + public void visitReturnStatement(final ReturnStatement statement) { + assertStatementAuthorized(statement); + statement.getExpression().visit(this); + } + + public void visitAssertStatement(final AssertStatement statement) { + assertStatementAuthorized(statement); + statement.getBooleanExpression().visit(this); + statement.getMessageExpression().visit(this); + } + + public void visitTryCatchFinally(final TryCatchStatement statement) { + assertStatementAuthorized(statement); + statement.getTryStatement().visit(this); + for (CatchStatement catchStatement : statement.getCatchStatements()) { + catchStatement.visit(this); + } + Statement finallyStatement = statement.getFinallyStatement(); + if (finallyStatement instanceof EmptyStatement) { + // dispatching to EmptyStatement will not call back visitor, + // must call our visitEmptyStatement explicitly + visitEmptyStatement((EmptyStatement) finallyStatement); + } else { + finallyStatement.visit(this); + } + } + + protected void visitEmptyStatement(EmptyStatement statement) { + // noop + } + + public void visitSwitch(final SwitchStatement statement) { + assertStatementAuthorized(statement); + statement.getExpression().visit(this); + for (CaseStatement caseStatement : statement.getCaseStatements()) { + caseStatement.visit(this); + } + statement.getDefaultStatement().visit(this); + } + + public void visitCaseStatement(final CaseStatement statement) { + assertStatementAuthorized(statement); + statement.getExpression().visit(this); + statement.getCode().visit(this); + } + + public void visitBreakStatement(final BreakStatement statement) { + assertStatementAuthorized(statement); + } + + public void visitContinueStatement(final ContinueStatement statement) { + assertStatementAuthorized(statement); + } + + public void visitThrowStatement(final ThrowStatement statement) { + assertStatementAuthorized(statement); + statement.getExpression().visit(this); + } + + public void visitSynchronizedStatement(final SynchronizedStatement statement) { + assertStatementAuthorized(statement); + statement.getExpression().visit(this); + statement.getCode().visit(this); + } + + public void visitCatchStatement(final CatchStatement statement) { + assertStatementAuthorized(statement); + statement.getCode().visit(this); + } + + public void visitMethodCallExpression(final MethodCallExpression call) { + assertExpressionAuthorized(call); + Expression receiver = call.getObjectExpression(); + final String typeName = receiver.getType().getName(); + if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) { + throw new SecurityException("Method calls not allowed on [" + typeName + "]"); + } else if (receiversBlackList != null && receiversBlackList.contains(typeName)) { + throw new SecurityException("Method calls not allowed on [" + typeName + "]"); + } + receiver.visit(this); + final Expression method = call.getMethod(); + checkConstantTypeIfNotMethodNameOrProperty(method); + call.getArguments().visit(this); + } + + public void visitStaticMethodCallExpression(final StaticMethodCallExpression call) { + assertExpressionAuthorized(call); + final String typeName = call.getOwnerType().getName(); + if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) { + throw new SecurityException("Method calls not allowed on [" + typeName + "]"); + } else if (receiversBlackList != null && receiversBlackList.contains(typeName)) { + throw new SecurityException("Method calls not allowed on [" + typeName + "]"); + } + call.getArguments().visit(this); + } + + public void visitConstructorCallExpression(final ConstructorCallExpression call) { + assertExpressionAuthorized(call); + call.getArguments().visit(this); + } + + public void visitTernaryExpression(final TernaryExpression expression) { + assertExpressionAuthorized(expression); + expression.getBooleanExpression().visit(this); + expression.getTrueExpression().visit(this); + expression.getFalseExpression().visit(this); + } + + public void visitShortTernaryExpression(final ElvisOperatorExpression expression) { + assertExpressionAuthorized(expression); + visitTernaryExpression(expression); + } + + public void visitBinaryExpression(final BinaryExpression expression) { + assertExpressionAuthorized(expression); + assertTokenAuthorized(expression.getOperation()); + expression.getLeftExpression().visit(this); + expression.getRightExpression().visit(this); + } + + public void visitPrefixExpression(final PrefixExpression expression) { + assertExpressionAuthorized(expression); + assertTokenAuthorized(expression.getOperation()); + expression.getExpression().visit(this); + } + + public void visitPostfixExpression(final PostfixExpression expression) { + assertExpressionAuthorized(expression); + assertTokenAuthorized(expression.getOperation()); + expression.getExpression().visit(this); + } + + public void visitBooleanExpression(final BooleanExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitClosureExpression(final ClosureExpression expression) { + assertExpressionAuthorized(expression); + if (!isClosuresAllowed) throw new SecurityException("Closures are not allowed"); + expression.getCode().visit(this); + } + + public void visitTupleExpression(final TupleExpression expression) { + assertExpressionAuthorized(expression); + visitListOfExpressions(expression.getExpressions()); + } + + public void visitMapExpression(final MapExpression expression) { + assertExpressionAuthorized(expression); + visitListOfExpressions(expression.getMapEntryExpressions()); + } + + public void visitMapEntryExpression(final MapEntryExpression expression) { + assertExpressionAuthorized(expression); + expression.getKeyExpression().visit(this); + expression.getValueExpression().visit(this); + } + + public void visitListExpression(final ListExpression expression) { + assertExpressionAuthorized(expression); + visitListOfExpressions(expression.getExpressions()); + } + + public void visitRangeExpression(final RangeExpression expression) { + assertExpressionAuthorized(expression); + expression.getFrom().visit(this); + expression.getTo().visit(this); + } + + public void visitPropertyExpression(final PropertyExpression expression) { + assertExpressionAuthorized(expression); + Expression receiver = expression.getObjectExpression(); + final String typeName = receiver.getType().getName(); + if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) { + throw new SecurityException("Property access not allowed on [" + typeName + "]"); + } else if (receiversBlackList != null && receiversBlackList.contains(typeName)) { + throw new SecurityException("Property access not allowed on [" + typeName + "]"); + } + receiver.visit(this); + final Expression property = expression.getProperty(); + checkConstantTypeIfNotMethodNameOrProperty(property); + } + + private void checkConstantTypeIfNotMethodNameOrProperty(final Expression expr) { + if (expr instanceof ConstantExpression) { + if (!"java.lang.String".equals(expr.getType().getName())) { + expr.visit(this); + } + } else { + expr.visit(this); + } + } + + public void visitAttributeExpression(final AttributeExpression expression) { + assertExpressionAuthorized(expression); + Expression receiver = expression.getObjectExpression(); + final String typeName = receiver.getType().getName(); + if (receiversWhiteList != null && !receiversWhiteList.contains(typeName)) { + throw new SecurityException("Attribute access not allowed on [" + typeName + "]"); + } else if (receiversBlackList != null && receiversBlackList.contains(typeName)) { + throw new SecurityException("Attribute access not allowed on [" + typeName + "]"); + } + receiver.visit(this); + final Expression property = expression.getProperty(); + checkConstantTypeIfNotMethodNameOrProperty(property); + } + + public void visitFieldExpression(final FieldExpression expression) { + assertExpressionAuthorized(expression); + } + + public void visitMethodPointerExpression(final MethodPointerExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + expression.getMethodName().visit(this); + } + + public void visitConstantExpression(final ConstantExpression expression) { + assertExpressionAuthorized(expression); + final String type = expression.getType().getName(); + if (constantTypesWhiteList != null && !constantTypesWhiteList.contains(type)) { + throw new SecurityException("Constant expression type [" + type + "] is not allowed"); + } + if (constantTypesBlackList != null && constantTypesBlackList.contains(type)) { + throw new SecurityException("Constant expression type [" + type + "] is not allowed"); + } + } + + public void visitClassExpression(final ClassExpression expression) { + assertExpressionAuthorized(expression); + } + + public void visitVariableExpression(final VariableExpression expression) { + assertExpressionAuthorized(expression); + final String type = expression.getType().getName(); + if (constantTypesWhiteList != null && !constantTypesWhiteList.contains(type)) { + throw new SecurityException("Usage of variables of type [" + type + "] is not allowed"); + } + if (constantTypesBlackList != null && constantTypesBlackList.contains(type)) { + throw new SecurityException("Usage of variables of type [" + type + "] is not allowed"); + } + } + + public void visitDeclarationExpression(final DeclarationExpression expression) { + assertExpressionAuthorized(expression); + visitBinaryExpression(expression); + } + + protected void visitListOfExpressions(List<? extends Expression> list) { + if (list == null) return; + for (Expression expression : list) { + if (expression instanceof SpreadExpression) { + Expression spread = ((SpreadExpression) expression).getExpression(); + spread.visit(this); + } else { + expression.visit(this); + } + } + } + + public void visitGStringExpression(final GStringExpression expression) { + assertExpressionAuthorized(expression); + visitListOfExpressions(expression.getStrings()); + visitListOfExpressions(expression.getValues()); + } + + public void visitArrayExpression(final ArrayExpression expression) { + assertExpressionAuthorized(expression); + visitListOfExpressions(expression.getExpressions()); + visitListOfExpressions(expression.getSizeExpression()); + } + + public void visitSpreadExpression(final SpreadExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitSpreadMapExpression(final SpreadMapExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitNotExpression(final NotExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitUnaryMinusExpression(final UnaryMinusExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitUnaryPlusExpression(final UnaryPlusExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitBitwiseNegationExpression(final BitwiseNegationExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitCastExpression(final CastExpression expression) { + assertExpressionAuthorized(expression); + expression.getExpression().visit(this); + } + + public void visitArgumentlistExpression(final ArgumentListExpression expression) { + assertExpressionAuthorized(expression); + visitTupleExpression(expression); + } + + public void visitClosureListExpression(final ClosureListExpression closureListExpression) { + assertExpressionAuthorized(closureListExpression); + if (!isClosuresAllowed) throw new SecurityException("Closures are not allowed"); + visitListOfExpressions(closureListExpression.getExpressions()); + } + + public void visitBytecodeExpression(final BytecodeExpression expression) { + assertExpressionAuthorized(expression); + } + } + + /** + * This interface allows the user to plugin custom expression checkers if expression blacklist or whitelist are not + * sufficient + */ + public interface ExpressionChecker { + boolean isAuthorized(Expression expression); + } + + /** + * This interface allows the user to plugin custom statement checkers if statement blacklist or whitelist are not + * sufficient + */ + public interface StatementChecker { + boolean isAuthorized(Statement expression); + } + +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/SourceAwareCustomizer.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/SourceAwareCustomizer.java b/src/main/java/org/codehaus/groovy/control/customizers/SourceAwareCustomizer.java new file mode 100644 index 0000000..3bb5183 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/SourceAwareCustomizer.java @@ -0,0 +1,105 @@ +/* + * 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.codehaus.groovy.control.customizers; + +import groovy.lang.Closure; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.io.FileReaderSource; +import org.codehaus.groovy.control.io.ReaderSource; + +/** + * A base class for customizers which only have to be applied on specific source units. + * This is for example useful if you want a customizer to be applied only for files + * matching some extensions. + * <p> + * For convenience, this class implements several methods that you may extend to customize + * the behaviour of this utility. For example, if you want to apply a customizer only + * for classes matching the '.foo' file extension, then you only have to override the + * {@link #acceptExtension(String)} method: + * <pre><code>return "foo".equals(extension)</code></pre> + * + * @since 2.1.0 + * @author Cedric Champeau + */ +public class SourceAwareCustomizer extends DelegatingCustomizer { + + private Closure<Boolean> extensionValidator; + private Closure<Boolean> baseNameValidator; + private Closure<Boolean> sourceUnitValidator; + private Closure<Boolean> classValidator; + + public SourceAwareCustomizer(CompilationCustomizer delegate) { + super(delegate); + } + + @Override + public void call(final SourceUnit source, final GeneratorContext context, final ClassNode classNode) throws CompilationFailedException { + String fileName = source.getName(); + ReaderSource reader = source.getSource(); + if (reader instanceof FileReaderSource) { + FileReaderSource file = (FileReaderSource) reader; + fileName = file.getFile().getName(); + } + if (acceptSource(source) && acceptClass(classNode) && accept(fileName)) { + delegate.call(source, context, classNode); + } + } + + public void setBaseNameValidator(final Closure<Boolean> baseNameValidator) { + this.baseNameValidator = baseNameValidator; + } + + public void setExtensionValidator(final Closure<Boolean> extensionValidator) { + this.extensionValidator = extensionValidator; + } + + public void setSourceUnitValidator(final Closure<Boolean> sourceUnitValidator) { + this.sourceUnitValidator = sourceUnitValidator; + } + + public void setClassValidator(final Closure<Boolean> classValidator) { + this.classValidator = classValidator; + } + + public boolean accept(String fileName) { + int ext = fileName.lastIndexOf("."); + String baseName = ext<0?fileName:fileName.substring(0, ext); + String extension = ext<0?"":fileName.substring(ext+1); + return acceptExtension(extension) && acceptBaseName(baseName); + } + + public boolean acceptClass(ClassNode cnode) { + return classValidator == null || classValidator.call(cnode); + } + + public boolean acceptSource(SourceUnit unit) { + return sourceUnitValidator==null || sourceUnitValidator.call(unit); + } + + public boolean acceptExtension(String extension) { + return extensionValidator==null || extensionValidator.call(extension); + } + + public boolean acceptBaseName(String baseName) { + return baseNameValidator==null || baseNameValidator.call(baseName); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/builder/CustomizersFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/CustomizersFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/CustomizersFactory.java new file mode 100644 index 0000000..40ce7e7 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/CustomizersFactory.java @@ -0,0 +1,59 @@ +/* + * 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.codehaus.groovy.control.customizers.builder; + +import groovy.util.AbstractFactory; +import groovy.util.FactoryBuilderSupport; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * This factory generates an array of compilation customizers. + * + * @author Cedric Champeau + * @since 2.1.0 + */ +public class CustomizersFactory extends AbstractFactory implements PostCompletionFactory { + + public Object newInstance(final FactoryBuilderSupport builder, final Object name, final Object value, final Map attributes) throws InstantiationException, IllegalAccessException { + return new LinkedList(); + } + + @Override + @SuppressWarnings("unchecked") + public void setChild(final FactoryBuilderSupport builder, final Object parent, final Object child) { + if (parent instanceof Collection && child instanceof CompilationCustomizer) { + ((Collection) parent).add(child); + } + } + + + @SuppressWarnings("unchecked") + public Object postCompleteNode(final FactoryBuilderSupport factory, final Object parent, final Object node) { + if (node instanceof List) { + List col = (List) node; + return col.toArray(new CompilationCustomizer[col.size()]); + } + return node; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java new file mode 100644 index 0000000..e9e8b7e --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/ImportCustomizerFactory.java @@ -0,0 +1,134 @@ +/* + * 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.codehaus.groovy.control.customizers.builder; + +import groovy.lang.Closure; +import groovy.lang.GString; +import groovy.util.AbstractFactory; +import groovy.util.FactoryBuilderSupport; +import org.codehaus.groovy.control.customizers.ImportCustomizer; + +import java.util.Collection; +import java.util.Map; + +/** + * This factory allows the generation of an {@link ImportCustomizer import customizer}. You may embed several + * elements: + * <ul> + * <li><i>normal</i> for "regular" imports</li> + * <li><i>star</i> for "star" imports</li> + * <li><i>staticStar</i> for "static star" imports</li> + * <li><i>alias</i> for imports with alias</li> + * <li><i>staticMember</i> for static imports of individual members</li> + * </ul> + * + * For example: + * <pre><code>builder.imports { + * alias 'AI', 'java.util.concurrent.atomic.AtomicInteger' + * alias 'AL', 'java.util.concurrent.atomic.AtomicLong' + *}</code></pre> + * + * @author Cedric Champeau + * @since 2.1.0 + */ +public class ImportCustomizerFactory extends AbstractFactory { + @Override + public boolean isHandlesNodeChildren() { + return true; + } + + public Object newInstance(final FactoryBuilderSupport builder, final Object name, final Object value, final Map attributes) throws InstantiationException, IllegalAccessException { + ImportCustomizer customizer = new ImportCustomizer(); + addImport(customizer, value); + return customizer; + } + + private void addImport(final ImportCustomizer customizer, final Object value) { + if (value==null) return; + if (value instanceof Collection) { + for (Object e : (Collection)value) { + addImport(customizer, e); + } + } else if (value instanceof String) { + customizer.addImports((String)value); + } else if (value instanceof Class) { + customizer.addImports(((Class) value).getName()); + } else if (value instanceof GString) { + customizer.addImports(value.toString()); + } else { + throw new RuntimeException("Unsupported import value type ["+value+"]"); + } + } + + @Override + public boolean onNodeChildren(final FactoryBuilderSupport builder, final Object node, final Closure childContent) { + if (node instanceof ImportCustomizer) { + Closure clone = (Closure) childContent.clone(); + clone.setDelegate(new ImportHelper((ImportCustomizer) node)); + clone.call(); + } + return false; + } + + private static final class ImportHelper { + private final ImportCustomizer customizer; + + private ImportHelper(final ImportCustomizer customizer) { + this.customizer = customizer; + } + + protected void normal(String... names) { + customizer.addImports(names); + } + protected void normal(Class... classes) { + for (Class aClass : classes) { + customizer.addImports(aClass.getName()); + } + } + + protected void alias(String alias, String name) { + customizer.addImport(alias, name); + } + protected void alias(String alias, Class clazz) { + customizer.addImport(alias, clazz.getName()); + } + + protected void star(String... packages) { + customizer.addStarImports(packages); + } + + protected void staticStar(String... classNames) { + customizer.addStaticStars(classNames); + } + protected void staticStar(Class... classes) { + for (Class aClass : classes) { + customizer.addStaticStars(aClass.getName()); + } + } + + protected void staticMember(String name, String field) { + customizer.addStaticImport(name, field); + } + protected void staticMember(String alias, String name, String field) { + customizer.addStaticImport(alias, name, field); + } + + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/builder/InlinedASTCustomizerFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/InlinedASTCustomizerFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/InlinedASTCustomizerFactory.java new file mode 100644 index 0000000..c7b599c --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/InlinedASTCustomizerFactory.java @@ -0,0 +1,89 @@ +/* + * 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.codehaus.groovy.control.customizers.builder; + +import groovy.lang.Closure; +import groovy.util.AbstractFactory; +import groovy.util.FactoryBuilderSupport; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.codehaus.groovy.runtime.ProxyGeneratorAdapter; + +import java.util.HashMap; +import java.util.Map; + +/** + * This factory lets a user define a compilation customizer without having to define + * an anonymous inner class. + * <p> + * Here is an example, which only logs the class name during compilation: + * <pre> + * inline(phase:'CONVERSION') { source, context, classNode -> + * println "visiting $classNode" + * } + * </pre> + * + * @author Cedric Champeau + * @since 2.1.0 + */ +@SuppressWarnings("unchecked") +public class InlinedASTCustomizerFactory extends AbstractFactory implements PostCompletionFactory { + + @Override + public boolean isHandlesNodeChildren() { + return true; + } + + public Object newInstance(final FactoryBuilderSupport builder, final Object name, final Object value, final Map attributes) throws InstantiationException, IllegalAccessException { + if (attributes.isEmpty() || !attributes.containsKey("phase")) { + throw new RuntimeException("You must specify a CompilePhase to run at, using the [phase] attribute"); + } + Map result = new HashMap(1+attributes.size()); + result.putAll(attributes); + return result; + } + + @Override + public boolean onNodeChildren(final FactoryBuilderSupport builder, final Object node, final Closure childContent) { + if (node instanceof Map) { + ((Map)node).put("call", childContent.clone()); + } + return false; + } + + public Object postCompleteNode(final FactoryBuilderSupport factory, final Object parent, final Object node) { + if (node instanceof Map) { + Map map = (Map) node; + ProxyGeneratorAdapter adapter = new ProxyGeneratorAdapter( + map, + map.containsKey("superClass")?(Class)map.get("superClass"):CompilationCustomizer.class, + map.containsKey("interfaces")?(Class[])map.get("interfaces"):null, + this.getClass().getClassLoader(), + false, + null + ); + Object phase = map.get("phase"); + if (!(phase instanceof CompilePhase)) { + phase = CompilePhase.valueOf(phase.toString()); + } + return adapter.proxy(map, phase); + } + return node; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/builder/PostCompletionFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/PostCompletionFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/PostCompletionFactory.java new file mode 100644 index 0000000..8855dbf --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/PostCompletionFactory.java @@ -0,0 +1,31 @@ +/* + * 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.codehaus.groovy.control.customizers.builder; + +import groovy.util.FactoryBuilderSupport; + +/** + * A helper interface for factories which require post processing of generated nodes. + * + * @since 2.1.0 + * @author Cedric Champeau + */ +public interface PostCompletionFactory { + Object postCompleteNode(FactoryBuilderSupport factory, Object parent, Object node); +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/builder/SecureASTCustomizerFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/SecureASTCustomizerFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/SecureASTCustomizerFactory.java new file mode 100644 index 0000000..3c81dee --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/SecureASTCustomizerFactory.java @@ -0,0 +1,55 @@ +/* + * 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.codehaus.groovy.control.customizers.builder; + +import groovy.lang.Closure; +import groovy.util.AbstractFactory; +import groovy.util.FactoryBuilderSupport; +import org.codehaus.groovy.control.customizers.SecureASTCustomizer; + +import java.util.Map; + +/** + * This factory allows the generation of a {@link SecureASTCustomizer}. Embedded elements are delegated + * to a {@link SecureASTCustomizer} instance. + * + * @since 2.1.0 + * @author Cedric Champeau + */ +public class SecureASTCustomizerFactory extends AbstractFactory { + @Override + public boolean isHandlesNodeChildren() { + return true; + } + + public Object newInstance(final FactoryBuilderSupport builder, final Object name, final Object value, final Map attributes) throws InstantiationException, IllegalAccessException { + return new SecureASTCustomizer(); + } + + @Override + public boolean onNodeChildren(final FactoryBuilderSupport builder, final Object node, final Closure childContent) { + if (node instanceof SecureASTCustomizer) { + Closure clone = (Closure) childContent.clone(); + clone.setDelegate(node); + clone.setResolveStrategy(Closure.DELEGATE_FIRST); + clone.call(); + } + return false; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/customizers/builder/SourceAwareCustomizerFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/customizers/builder/SourceAwareCustomizerFactory.java b/src/main/java/org/codehaus/groovy/control/customizers/builder/SourceAwareCustomizerFactory.java new file mode 100644 index 0000000..d0fc541 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/customizers/builder/SourceAwareCustomizerFactory.java @@ -0,0 +1,160 @@ +/* + * 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.codehaus.groovy.control.customizers.builder; + +import groovy.lang.Closure; +import groovy.util.AbstractFactory; +import groovy.util.FactoryBuilderSupport; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.codehaus.groovy.control.customizers.SourceAwareCustomizer; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Factory for use with {@link CompilerCustomizationBuilder}. Allows the construction of {@link SourceAwareCustomizer + * source aware customizers}. Syntax: + * <pre><code> + * // apply CompileStatic AST annotation on .sgroovy files + * builder.source(extension: 'sgroovy') { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on .sgroovy or .sg files + * builder.source(extensions: ['sgroovy','sg']) { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on .sgroovy or .sg files + * builder.source(extensionValidator: { it.name in ['sgroovy','sg']}) { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on files whose name is 'foo' + * builder.source(basename: 'foo') { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on files whose name is 'foo' or 'bar' + * builder.source(basenames: ['foo', 'bar']) { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on files whose name is 'foo' or 'bar' + * builder.source(basenameValidator: { it in ['foo', 'bar'] }) { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on files that do not contain a class named 'Baz' + * builder.source(unitValidator: { unit -> !unit.AST.classes.any { it.name == 'Baz' } }) { + * ast(CompileStatic) + * } + * + * // apply CompileStatic AST annotation on class nodes that end with 'CS' + * builder.source(classValidator: { cn -> cn.name.endsWith('CS') }) { + * ast(CompileStatic) + * } + * </code></pre> + * + * @author Cedric Champeau + */ +public class SourceAwareCustomizerFactory extends AbstractFactory implements PostCompletionFactory { + + public Object newInstance(final FactoryBuilderSupport builder, final Object name, final Object value, final Map attributes) throws InstantiationException, IllegalAccessException { + SourceOptions data = new SourceOptions(); + if (value instanceof CompilationCustomizer) { + data.delegate = (CompilationCustomizer) value; + } + return data; + } + + @Override + public void setChild(final FactoryBuilderSupport builder, final Object parent, final Object child) { + if (child instanceof CompilationCustomizer && parent instanceof SourceOptions) { + ((SourceOptions) parent).delegate = (CompilationCustomizer) child; + } + } + + public Object postCompleteNode(final FactoryBuilderSupport factory, final Object parent, final Object node) { + SourceOptions data = (SourceOptions) node; + SourceAwareCustomizer sourceAwareCustomizer = new SourceAwareCustomizer(data.delegate); + if (data.extensionValidator !=null && (data.extension!=null || data.extensions!=null)) { + throw new RuntimeException("You must choose between an extension name validator or an explicit extension name"); + } + if (data.basenameValidator!=null && (data.basename!=null || data.basenames!=null)) { + throw new RuntimeException("You must choose between an base name validator or an explicit base name"); + } + + addExtensionValidator(sourceAwareCustomizer, data); + addBasenameValidator(sourceAwareCustomizer, data); + if (data.unitValidator!=null) sourceAwareCustomizer.setSourceUnitValidator(data.unitValidator); + if (data.classValidator!=null) sourceAwareCustomizer.setClassValidator(data.classValidator); + return sourceAwareCustomizer; + } + + private static void addExtensionValidator(final SourceAwareCustomizer sourceAwareCustomizer, final SourceOptions data) { + final List<String> extensions = data.extensions!=null?data.extensions : new LinkedList<String>(); + if (data.extension!=null) extensions.add(data.extension); + Closure<Boolean> extensionValidator = data.extensionValidator; + if (extensionValidator==null && !extensions.isEmpty()) { + extensionValidator = new Closure<Boolean>(sourceAwareCustomizer) { + @Override + @SuppressWarnings("unchecked") + public Boolean call(final Object arguments) { + return extensions.contains(arguments); + } + }; + } + sourceAwareCustomizer.setExtensionValidator(extensionValidator); + } + + private static void addBasenameValidator(final SourceAwareCustomizer sourceAwareCustomizer, final SourceOptions data) { + final List<String> basenames = data.basenames!=null?data.basenames : new LinkedList<String>(); + if (data.basename!=null) basenames.add(data.basename); + Closure<Boolean> basenameValidator = data.basenameValidator; + if (basenameValidator==null && !basenames.isEmpty()) { + basenameValidator = new Closure<Boolean>(sourceAwareCustomizer) { + @Override + @SuppressWarnings("unchecked") + public Boolean call(final Object arguments) { + return basenames.contains(arguments); + } + }; + } + sourceAwareCustomizer.setBaseNameValidator(basenameValidator); + } + + public static class SourceOptions { + public CompilationCustomizer delegate; + // validate with closures + public Closure<Boolean> extensionValidator; + public Closure<Boolean> unitValidator; + public Closure<Boolean> basenameValidator; + public Closure<Boolean> classValidator; + + // validate with one string + public String extension; + public String basename; + + // validate with list of strings + public List<String> extensions; + public List<String> basenames; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/control/io/AbstractReaderSource.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/control/io/AbstractReaderSource.java b/src/main/java/org/codehaus/groovy/control/io/AbstractReaderSource.java new file mode 100644 index 0000000..1c1084c --- /dev/null +++ b/src/main/java/org/codehaus/groovy/control/io/AbstractReaderSource.java @@ -0,0 +1,120 @@ +/* + * 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.codehaus.groovy.control.io; + +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.Janitor; + +import java.io.BufferedReader; +import java.io.IOException; + +/** + * For ReaderSources that can choose a parent class, a base that + * provides common functionality. + * + * @author <a href="mailto:[email protected]">Chris Poirier</a> + */ + +public abstract class AbstractReaderSource implements ReaderSource { + protected CompilerConfiguration configuration; // Configuration data + + public AbstractReaderSource(CompilerConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("Compiler configuration must not be null!"); + // ... or more relaxed? + // configuration = CompilerConfiguration.DEFAULT; + } + this.configuration = configuration; + } + + /** + * Returns true if the source can be restarted (ie. if getReader() + * will return non-null on subsequent calls. + */ + public boolean canReopenSource() { + return true; + } + + private BufferedReader lineSource = null; // If set, a reader on the current source file + private String line = null; // The last line read from the current source file + private int number = 0; // The last line number read + + /** + * Returns a line from the source, or null, if unavailable. If + * you supply a Janitor, resources will be cached. + */ + public String getLine(int lineNumber, Janitor janitor) { + // If the source is already open and is passed the line we + // want, close it. + if (lineSource != null && number > lineNumber) { + cleanup(); + } + + // If the line source is closed, try to open it. + if (lineSource == null) { + try { + lineSource = new BufferedReader(getReader()); + } catch (Exception e) { + // Ignore + } + number = 0; + } + + // Read until the appropriate line number. + if (lineSource != null) { + while (number < lineNumber) { + try { + line = lineSource.readLine(); + number++; + } + catch (IOException e) { + cleanup(); + } + } + + if (janitor == null) { + final String result = line; // otherwise cleanup() will wipe out value + cleanup(); + return result; + } else { + janitor.register(this); + } + } + + return line; + } + + /** + * Cleans up any cached resources used by getLine(). + */ + public void cleanup() { + if (lineSource != null) { + try { + lineSource.close(); + } catch (Exception e) { + // Ignore + } + } + + lineSource = null; + line = null; + number = 0; + } + +}
