/* * Copyright (C) 2009 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package lombok.patcher.scripts; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import lombok.patcher.Hook; import lombok.patcher.StackRequest; import lombok.patcher.TargetMatcher; import org.objectweb.asm.Opcodes; public class ScriptBuilder { private ScriptBuilder() throws NoSuchMethodException { throw new NoSuchMethodException("ScriptBuilder cannot be instantiated - just use the static methods."); } private static void checkTypeSyntaxSlash(String spec) { if (spec.indexOf('.') > -1) throw new IllegalArgumentException( "Your type specification includes a dot, but this method wants a slash-separated type specification"); } private static void checkTypeSyntaxDot(String spec) { if (spec.indexOf('/') > -1) throw new IllegalArgumentException( "Your type specification includes a slash, but this method wants a dot-separated type specification"); } public static class AddFieldBuilder { private int accessFlags; private List<String> targetClasses = new ArrayList<String>(); private String fieldName; private String fieldType; private Object value; private static final int NO_ACCESS_LEVELS = ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PRIVATE | Opcodes.ACC_PRIVATE); public AddFieldScript build() { if (targetClasses.isEmpty()) throw new IllegalStateException("You have to set at least one targetClass."); if (fieldName == null) throw new IllegalStateException("You have to set a fieldName"); if (fieldType == null) throw new IllegalStateException("You have to set the new field's type by calling fieldType"); if (value != null) { setStatic(); setFinal(); } return new AddFieldScript(targetClasses, accessFlags, fieldName, fieldType, value); } /** * @param targetClass The class to add the field to, separated with dots (e.g. java.lang.String). */ public AddFieldBuilder targetClass(String targetClass) { checkTypeSyntaxDot(targetClass); this.targetClasses.add(targetClass); return this; } public AddFieldBuilder value(Object value) { this.value = value; return this; } /** * @param fieldName the name of the field to create. */ public AddFieldBuilder fieldName(String fieldName) { this.fieldName = fieldName; return this; } /** * @param fieldType the type of the field, in JVM spec (e.g. [I for an int array). */ public AddFieldBuilder fieldType(String fieldType) { checkTypeSyntaxSlash(fieldType); this.fieldType = fieldType; return this; } public AddFieldBuilder setPublic() { accessFlags = (accessFlags & NO_ACCESS_LEVELS) | Opcodes.ACC_PUBLIC; return this; } public AddFieldBuilder setPrivate() { accessFlags = (accessFlags & NO_ACCESS_LEVELS) | Opcodes.ACC_PRIVATE; return this; } public AddFieldBuilder setProtected() { accessFlags = (accessFlags & NO_ACCESS_LEVELS) | Opcodes.ACC_PROTECTED; return this; } public AddFieldBuilder setPackageAccess() { accessFlags = (accessFlags & NO_ACCESS_LEVELS); return this; } public AddFieldBuilder setStatic() { accessFlags |= Opcodes.ACC_STATIC; return this; } public AddFieldBuilder setFinal() { accessFlags |= Opcodes.ACC_FINAL; return this; } public AddFieldBuilder setVolatile() { accessFlags |= Opcodes.ACC_VOLATILE; return this; } public AddFieldBuilder setTransient() { accessFlags |= Opcodes.ACC_TRANSIENT; return this; } } public static class ExitEarlyBuilder { private List<TargetMatcher> matchers = new ArrayList<TargetMatcher>(); private Hook decisionMethod, valueMethod; private Set<StackRequest> requests = new HashSet<StackRequest>(); private boolean transplant, insert; public ExitFromMethodEarlyScript build() { if (matchers.isEmpty()) throw new IllegalStateException("You have to set a target method matcher"); return new ExitFromMethodEarlyScript(matchers, decisionMethod, valueMethod, transplant, insert, requests); } public ExitEarlyBuilder target(TargetMatcher matcher) { this.matchers.add(matcher); return this; } public ExitEarlyBuilder decisionMethod(Hook hook) { this.decisionMethod = hook; return this; } public ExitEarlyBuilder valueMethod(Hook hook) { this.valueMethod = hook; return this; } public ExitEarlyBuilder transplant() { this.transplant = true; this.insert = false; return this; } public ExitEarlyBuilder insert() { this.transplant = false; this.insert = true; return this; } public ExitEarlyBuilder request(StackRequest... requests) { for (StackRequest r : requests) { if (r == StackRequest.RETURN_VALUE) throw new IllegalArgumentException( "You cannot ask for the tentative return value in ExitFromMethodEarlyScript."); this.requests.add(r); } return this; } } public static class ReplaceMethodCallBuilder { private List<TargetMatcher> matchers = new ArrayList<TargetMatcher>(); private Hook replacementMethod; private Hook methodToReplace; private Set<StackRequest> extraRequests = new HashSet<StackRequest>(); private boolean transplant, insert; public ReplaceMethodCallScript build() { if (matchers.isEmpty()) throw new IllegalStateException("You have to set a target method matcher"); if (replacementMethod == null) throw new IllegalStateException("You have to set a replacement method"); if (methodToReplace == null) throw new IllegalStateException("You have to set a method call to replace"); return new ReplaceMethodCallScript(matchers, methodToReplace, replacementMethod, transplant, insert, extraRequests); } public ReplaceMethodCallBuilder target(TargetMatcher matcher) { this.matchers.add(matcher); return this; } public ReplaceMethodCallBuilder replacementMethod(Hook hook) { this.replacementMethod = hook; return this; } public ReplaceMethodCallBuilder methodToReplace(Hook hook) { this.methodToReplace = hook; return this; } public ReplaceMethodCallBuilder transplant() { this.transplant = true; this.insert = false; return this; } public ReplaceMethodCallBuilder insert() { this.transplant = false; this.insert = true; return this; } public ReplaceMethodCallBuilder requestExtra(StackRequest... requests) { for (StackRequest r : requests) { if (r == StackRequest.RETURN_VALUE) throw new IllegalArgumentException( "You cannot ask for the tentative return value in ReplaceMethodCallScript."); this.extraRequests.add(r); } return this; } } public static class WrapMethodCallBuilder { private List<TargetMatcher> matchers = new ArrayList<TargetMatcher>(); private Hook wrapMethod; private Hook methodToWrap; private Set<StackRequest> extraRequests = new HashSet<StackRequest>(); private boolean transplant, insert; public WrapMethodCallScript build() { if (matchers.isEmpty()) throw new IllegalStateException("You have to set a target method matcher"); if (wrapMethod == null) throw new IllegalStateException("You have to set method to wrap with"); if (methodToWrap == null) throw new IllegalStateException("You have to set a method call to wrap"); return new WrapMethodCallScript(matchers, methodToWrap, wrapMethod, transplant, insert, extraRequests); } public WrapMethodCallBuilder target(TargetMatcher matcher) { this.matchers.add(matcher); return this; } public WrapMethodCallBuilder wrapMethod(Hook hook) { this.wrapMethod = hook; return this; } public WrapMethodCallBuilder methodToWrap(Hook hook) { this.methodToWrap = hook; return this; } public WrapMethodCallBuilder transplant() { this.transplant = true; this.insert = false; return this; } public WrapMethodCallBuilder insert() { this.transplant = false; this.insert = true; return this; } public WrapMethodCallBuilder requestExtra(StackRequest... requests) { for (StackRequest r : requests) { if (r == StackRequest.RETURN_VALUE) throw new IllegalArgumentException( "You cannot ask for the tentative return value in WrapMethodCallBuilder."); this.extraRequests.add(r); } return this; } } public static class WrapReturnValueBuilder { private List<TargetMatcher> matchers = new ArrayList<TargetMatcher>(); private Hook wrapMethod; private Set<StackRequest> requests = new HashSet<StackRequest>(); private boolean transplant, insert; public WrapReturnValuesScript build() { if (matchers.isEmpty()) throw new IllegalStateException("You have to set a target method matcher"); if (wrapMethod == null) throw new IllegalStateException("You have to set a method you'd like to wrap the return values with"); return new WrapReturnValuesScript(matchers, wrapMethod, transplant, insert, requests); } public WrapReturnValueBuilder target(TargetMatcher matcher) { this.matchers.add(matcher); return this; } public WrapReturnValueBuilder wrapMethod(Hook hook) { this.wrapMethod = hook; return this; } public WrapReturnValueBuilder transplant() { this.transplant = true; this.insert = false; return this; } public WrapReturnValueBuilder insert() { this.transplant = false; this.insert = true; return this; } public WrapReturnValueBuilder request(StackRequest... requests) { for (StackRequest r : requests) this.requests.add(r); return this; } } public static class SetSymbolDuringMethodCallBuilder { private List<TargetMatcher> matchers = new ArrayList<TargetMatcher>(); private Hook callToWrap; private String symbol; private boolean report; public SetSymbolDuringMethodCallScript build() { if (matchers.isEmpty()) throw new IllegalStateException("You have to set a target method matcher"); if (callToWrap == null) throw new IllegalStateException("You have to set a method that needs to set the symbol during its invocation"); if (symbol == null) throw new IllegalStateException("You have to specify the symbol that is on the stack during callToWrap's invocation"); return new SetSymbolDuringMethodCallScript(matchers, callToWrap, symbol, report); } public SetSymbolDuringMethodCallBuilder target(TargetMatcher matcher) { this.matchers.add(matcher); return this; } public SetSymbolDuringMethodCallBuilder callToWrap(Hook callToWrap) { this.callToWrap = callToWrap; return this; } public SetSymbolDuringMethodCallBuilder symbol(String symbol) { this.symbol = symbol; return this; } public SetSymbolDuringMethodCallBuilder report() { this.report = true; return this; } } /** * Adds a field to any class. */ public static AddFieldBuilder addField() { return new AddFieldBuilder(); } /** * Allows you patch any method so that you get called first, and you can choose to take over entirely if you want. */ public static ExitEarlyBuilder exitEarly() { return new ExitEarlyBuilder(); } /** * Allows you to replace all calls to a given method in a given method with calls to a method of your choosing. */ public static ReplaceMethodCallBuilder replaceMethodCall() { return new ReplaceMethodCallBuilder(); } /** * Allows you to inspect and optionally replace the result of calls to a given method in a given method. */ public static WrapMethodCallBuilder wrapMethodCall() { return new WrapMethodCallBuilder(); } /** * Allows you to inspect every value right before it is returned, and, optionally, replace it with something else. */ public static WrapReturnValueBuilder wrapReturnValue() { return new WrapReturnValueBuilder(); } /** * Allows you to push a symbol for the duration of all calls to method A in method B. */ public static SetSymbolDuringMethodCallBuilder setSymbolDuringMethodCall() { return new SetSymbolDuringMethodCallBuilder(); } }