Index: plugins/org.chromium.debug.ui/src/org/chromium/debug/ui/DialogUtils.java |
diff --git a/plugins/org.chromium.debug.ui/src/org/chromium/debug/ui/DialogUtils.java b/plugins/org.chromium.debug.ui/src/org/chromium/debug/ui/DialogUtils.java |
index c3f7c2132042bc0ca78877f52b07790b027932eb..17d5d0c7de6c064ca8a1bde2943a26be90cbe13f 100644 |
--- a/plugins/org.chromium.debug.ui/src/org/chromium/debug/ui/DialogUtils.java |
+++ b/plugins/org.chromium.debug.ui/src/org/chromium/debug/ui/DialogUtils.java |
@@ -4,6 +4,15 @@ |
package org.chromium.debug.ui; |
+import java.lang.annotation.ElementType; |
+import java.lang.annotation.Retention; |
+import java.lang.annotation.RetentionPolicy; |
+import java.lang.annotation.Target; |
+import java.lang.reflect.InvocationTargetException; |
+import java.lang.reflect.Method; |
+import java.lang.reflect.ParameterizedType; |
+import java.lang.reflect.Type; |
+import java.lang.reflect.WildcardType; |
import java.util.ArrayList; |
import java.util.Arrays; |
import java.util.Collection; |
@@ -310,7 +319,7 @@ public class DialogUtils { |
* Creates a switcher that is operated by optional expression. |
* @param <T> type of expression |
*/ |
- <T> OptionalSwitcher<T> addOptionalSwitch(Gettable<? extends Optional<T>> expression); |
+ <T> OptionalSwitcher<T> addOptionalSwitch(Gettable<? extends Optional<? extends T>> expression); |
/** |
* Creates a switcher that is operated by non-optional expression. |
@@ -358,8 +367,8 @@ public class DialogUtils { |
* all sources have optional type and the merge source itself of optional type. The switcher |
* expression may have error value, in this case the merger also returns this error value. |
*/ |
- <P> ValueSource<? extends Optional<P>> createOptionalMerge( |
- ValueSource<? extends Optional<P>> ... sources); |
+ <P> ValueSource<? extends Optional<? extends P>> createOptionalMerge( |
+ ValueSource<? extends Optional<? extends P>> ... sources); |
} |
public static <T> ValueSource<T> createConstant(final T constnant, Updater updater) { |
@@ -403,6 +412,28 @@ public class DialogUtils { |
public boolean isNormal() { |
return true; |
} |
+ @Override |
+ public boolean equals(Object obj) { |
+ if (obj == null) { |
+ return false; |
+ } |
+ if (obj == this) { |
+ return true; |
+ } |
+ if (!obj.getClass().equals(this.getClass())) { |
+ return false; |
+ } |
+ Optional<?> other = (Optional<?>) obj; |
+ if (value == null) { |
+ return other.getNormal() == null; |
+ } else { |
+ return value.equals(other.getNormal()); |
+ } |
+ } |
+ @Override |
+ public int hashCode() { |
+ return value == null ? 0 : value.hashCode(); |
+ } |
}; |
} |
@@ -421,6 +452,28 @@ public class DialogUtils { |
public boolean isNormal() { |
return false; |
} |
+ @Override |
+ public boolean equals(Object obj) { |
+ if (obj == null) { |
+ return false; |
+ } |
+ if (obj == this) { |
+ return true; |
+ } |
+ if (!obj.getClass().equals(this.getClass())) { |
+ return false; |
+ } |
+ Optional<?> other = (Optional<?>) obj; |
+ if (messages == null) { |
+ return other.errorMessages() == null; |
+ } else { |
+ return messages.equals(other.errorMessages()); |
+ } |
+ } |
+ @Override |
+ public int hashCode() { |
+ return messages.hashCode(); |
+ } |
}; |
} |
@@ -502,70 +555,225 @@ public class DialogUtils { |
} |
/** |
- * An interface similar to {@link Gettable}, but with a quite specific contract: |
- * it may depend on some optional values, but its calculate method should only be called |
- * when all of the values are normal (non-error). This way its implementations becomes simpler. |
- * It's up to someone who calls calculate method to check that contract is held. |
+ * An expression that gets calculated only when its dependencies are all non-error. |
+ * The interface contains a "calculate" method and several methods that return expression |
+ * dependencies. |
+ * <p> |
+ * The one that is using this expression is responsible for reading all sources, checking |
+ * whether the optional values are normal and calling "calculate" method passing the |
+ * normal values as arguments. |
+ * <p> |
+ * The interface is reflection-oriented, as you cannot express type schema in plain Java. |
+ * The client should check the interface for a type-consistency statically on runtime and |
+ * throw exception if something is wrong. All user types are explicitly declared in |
+ * signature of methods. This allows accurate type checking via reflection (including |
+ * generics parameters, which are otherwise erased on runtime). |
+ * <p> |
+ * The interface is reflection-based. It only contains annotation that method should have. |
+ * |
+ * @param <T> type of value this expression returns |
*/ |
- public interface NormalExpression<RES> { |
- RES calculate(); |
- } |
+ public interface NormalExpression<T> { |
+ /** |
+ * An annotation for a "calculate" method of the interface. There should be only one such |
+ * a method in the object. Its return type should be "T" or "Optional<? extends T>" (we are |
+ * flexible in this only place). It should have arguments one per its dependency |
+ * (in the same order). |
+ * <p>The method must be declared public for the reflection to work. |
+ */ |
+ @Retention(RetentionPolicy.RUNTIME) |
+ @Target(ElementType.METHOD) |
+ @interface Calculate { |
+ } |
- /** |
- * Creates a {@link ValueProcessor} that is backed by {@link NormalExpression}. |
- * @param optionalSources list of inputs that are optional and thus have to be checked for |
- * {@link NormalExpression} contract. |
- */ |
- public static <T> ValueProcessor<Optional<T>> createOptionalProcessor( |
- final NormalExpression<T> expression, |
- ValueSource<? extends Optional<?>> ... optionalSources) { |
- final Gettable<Optional<T>> getter = handleErrors(expression, optionalSources); |
- return createProcessor(getter); |
+ /** |
+ * An annotation for a method that returns expression dependency. It should have no arguments |
+ * and return IValueSource<? extends Optional<*T*>> type ("IValueSource" is significant here). |
+ * The type *T* should correspond to the type of "calculate" method argument (dependency |
+ * methods should go in the same order as "calculate" arguments go). |
+ * <p>The method must be declared public for the reflection to work. |
+ */ |
+ @Target(ElementType.METHOD) |
+ @Retention(RetentionPolicy.RUNTIME) |
+ @interface DependencyGetter { |
+ } |
} |
+ |
/** |
- * Implements the basic contract of {@link NormalExpression}. Wraps it as {@link Gettable} and |
- * keeps all its optional sources that are checked before each calculation. |
+ * Converts {@link NormalExpression} into {@link Gettable} and takes responsibility of checking |
+ * that all dependencies have only normal values. Despite {@link NormalExpression} being |
+ * reflection-based interface, this method should be completely type-safe for a programmer and |
+ * accurately check (statically) that its signatures are consistent (including generic types). |
*/ |
- public static <RES> Gettable<Optional<RES>> handleErrors(final NormalExpression<RES> expression, |
- final ValueSource<? extends Optional<?>> ... optionalSources) { |
- NormalExpression<Optional<RES>> wrapper = new NormalExpression<Optional<RES>>() { |
- public Optional<RES> calculate() { |
- return createOptional(expression.calculate()); |
+ public static <RES> Gettable<Optional<? extends RES>> handleErrors( |
+ final NormalExpression<RES> expression) { |
+ Class<?> expressionClass = expression.getClass(); |
+ |
+ // All reflection is done in generic-aware API generation. |
+ |
+ // Read generic NormalExpression type parameter of expression class. |
+ Type expressionType; |
+ { |
+ ParameterizedType normalExpressionType = null; |
+ for (Type inter : expressionClass.getGenericInterfaces()) { |
+ if (inter instanceof ParameterizedType == false) { |
+ continue; |
+ } |
+ ParameterizedType parameterizedType = (ParameterizedType) inter; |
+ if (!parameterizedType.getRawType().equals(NormalExpression.class)) { |
+ continue; |
+ } |
+ normalExpressionType = parameterizedType; |
} |
- }; |
- return handleErrorsAddNew(wrapper, optionalSources); |
- } |
+ if (normalExpressionType == null) { |
+ throw new IllegalArgumentException("Expression does not directly implement " + |
+ NormalExpression.class.getName()); |
+ } |
+ expressionType = normalExpressionType.getActualTypeArguments()[0]; |
+ } |
- /** |
- * Implements the basic contract of {@link NormalExpression}. Wraps it as {@link Gettable} and |
- * keeps all its optional sources that are checked before each calculation. |
- * The expression may rely on all optionalSources being of normal values, but it is allowed to |
- * return error value itself. |
- */ |
- public static <RES> Gettable<Optional<RES>> handleErrorsAddNew( |
- final NormalExpression<Optional<RES>> expression, |
- final ValueSource<? extends Optional<?>> ... optionalSources) { |
- return new Gettable<Optional<RES>>() { |
- public Optional<RES> getValue() { |
- boolean hasErrors = false; |
- for (ValueSource<? extends Optional<?>> source : optionalSources) { |
- if (!source.getValue().isNormal()) { |
- hasErrors = true; |
- break; |
+ // Read all methods of expression class and choose annotated ones. |
+ Method calculateMethodVar = null; |
+ final List<Method> dependencyMethods = new ArrayList<Method>(2); |
+ for (Method m : expressionClass.getMethods()) { |
+ if (m.getAnnotation(NormalExpression.Calculate.class) != null) { |
+ if (calculateMethodVar != null) { |
+ throw new IllegalArgumentException("Class " + expressionClass.getName() + |
+ " has more than one method with " + |
+ NormalExpression.Calculate.class.getName() + " annotation"); |
+ } |
+ calculateMethodVar = m; |
+ } |
+ if (m.getAnnotation(NormalExpression.DependencyGetter.class) != null) { |
+ dependencyMethods.add(m); |
+ } |
+ } |
+ if (calculateMethodVar == null) { |
+ throw new IllegalArgumentException("Failed to found Class method with " + |
+ NormalExpression.Calculate.class.getName() + " annotation in " + |
+ expressionClass.getName()); |
+ } |
+ final Method calculateMethod = calculateMethodVar; |
+ Type methodReturnType = calculateMethod.getGenericReturnType(); |
+ |
+ // Method is typically in anonymous class. Making it accessible is required. |
+ calculateMethod.setAccessible(true); |
+ |
+ // Prepare handling method return value (it's either a plain value or an optional wrapper). |
+ abstract class ReturnValueHandler { |
+ abstract Optional<? extends RES> castResult(Object resultObject); |
+ } |
+ |
+ final ReturnValueHandler returnValueHandler; |
+ |
+ if (methodReturnType.equals(expressionType)) { |
+ returnValueHandler = new ReturnValueHandler() { |
+ Optional<? extends RES> castResult(Object resultObject) { |
+ // Return type in interface is RES. |
+ // Type cast has been proven to be correct. |
+ return createOptional((RES) resultObject); |
+ } |
+ }; |
+ } else { |
+ tryUnwrapOptional: { |
+ if (methodReturnType instanceof ParameterizedType) { |
+ ParameterizedType parameterizedType = (ParameterizedType) methodReturnType; |
+ if (parameterizedType.getRawType() == Optional.class) { |
+ Type optionalParam = parameterizedType.getActualTypeArguments()[0]; |
+ boolean okToCast = false; |
+ if (optionalParam instanceof WildcardType) { |
+ WildcardType wildcardType = (WildcardType) optionalParam; |
+ if (wildcardType.getUpperBounds()[0].equals(expressionType)) { |
+ okToCast = true; |
+ } |
+ } else if (optionalParam.equals(expressionType)) { |
+ okToCast = true; |
+ } |
+ if (okToCast) { |
+ returnValueHandler = new ReturnValueHandler() { |
+ Optional<? extends RES> castResult(Object resultObject) { |
+ // Return type in interface is optional wrapper around RES. |
+ // Type cast has been proven to be correct. |
+ return (Optional<? extends RES>) resultObject; |
+ } |
+ }; |
+ break tryUnwrapOptional; |
+ } |
} |
} |
+ throw new IllegalArgumentException("Wrong return type " + methodReturnType + |
+ ", expected: " + expressionType); |
+ } |
+ } |
+ |
+ // Check that dependencies correspond to "calculate" method arguments. |
+ Type[] methodParamTypes = calculateMethod.getGenericParameterTypes(); |
+ if (methodParamTypes.length != dependencyMethods.size()) { |
+ throw new IllegalArgumentException("Wrong number of agruments in calculate method " + |
+ calculateMethod); |
+ } |
+ // We depend on methods being ordered in Java reflection. |
+ for (int i = 0; i < methodParamTypes.length; i++) { |
+ Method depMethod = dependencyMethods.get(i); |
+ try { |
+ if (depMethod.getParameterTypes().length != 0) { |
+ throw new IllegalArgumentException("Dependency method should not have arguments"); |
+ } |
+ Type depType = depMethod.getGenericReturnType(); |
+ if (depType instanceof ParameterizedType == false) { |
+ throw new IllegalArgumentException("Dependency has wrong return type: " + depType); |
+ } |
+ ParameterizedType depParameterizedType = (ParameterizedType) depType; |
+ if (depParameterizedType.getRawType() != ValueSource.class) { |
+ throw new IllegalArgumentException("Dependency has wrong return type: " + depType); |
+ } |
+ // Method is typically in anonymous class. Making it accessible is required. |
+ depMethod.setAccessible(true); |
+ } catch (IllegalArgumentException e) { |
+ throw new IllegalArgumentException("Failed to process method " + depMethod, e); |
+ } |
+ } |
- if (hasErrors) { |
- Set<Message> errors = new LinkedHashSet<Message>(0); |
- for (ValueSource<? extends Optional<?>> source : optionalSources) { |
- if (!source.getValue().isNormal()) { |
- errors.addAll(source.getValue().errorMessages()); |
+ // Create implementation that will call methods via reflection. |
+ return new Gettable<Optional<? extends RES>>() { |
+ @Override |
+ public Optional<? extends RES> getValue() { |
+ Object[] params = new Object[dependencyMethods.size()]; |
+ Set<Message> errors = null; |
+ for (int i = 0; i < params.length; i++) { |
+ Object sourceObject; |
+ try { |
+ sourceObject = dependencyMethods.get(i).invoke(expression); |
+ } catch (IllegalAccessException e) { |
+ throw new RuntimeException(e); |
+ } catch (InvocationTargetException e) { |
+ throw new RuntimeException(e); |
+ } |
+ ValueSource<? extends Optional<?>> source = |
+ (ValueSource<? extends Optional<?>>) sourceObject; |
+ Optional<?> optionalValue = source.getValue(); |
+ if (optionalValue.isNormal()) { |
+ params[i] = optionalValue.getNormal(); |
+ } else { |
+ if (errors == null) { |
+ errors = new LinkedHashSet<Message>(0); |
} |
+ errors.addAll(optionalValue.errorMessages()); |
} |
- return createErrorOptional(errors); |
+ } |
+ if (errors == null) { |
+ Object result; |
+ try { |
+ result = calculateMethod.invoke(expression, params); |
+ } catch (IllegalAccessException e) { |
+ throw new RuntimeException(e); |
+ } catch (InvocationTargetException e) { |
+ throw new RuntimeException(e); |
+ } |
+ return returnValueHandler.castResult(result); |
} else { |
- return expression.calculate(); |
+ return createErrorOptional(errors); |
} |
} |
}; |
@@ -871,21 +1079,23 @@ public class DialogUtils { |
setCurrentScope(newScope); |
} |
- public <P> ValueSource<? extends Optional<P>> createOptionalMerge( |
- ValueSource<? extends Optional<P>>... sources) { |
+ public <P> ValueSource<? extends Optional<? extends P>> createOptionalMerge( |
+ ValueSource<? extends Optional<? extends P>>... sources) { |
- final Map<T, ValueSource<? extends Optional<P>>> map = sortSources(Arrays.asList(sources)); |
+ final Map<T, ValueSource<? extends Optional<? extends P>>> map = |
+ sortSources(Arrays.asList(sources)); |
- ValueProcessor<? extends Optional<P>> result = new ValueProcessor<Optional<P>>() { |
+ ValueProcessor<? extends Optional<? extends P>> result = |
+ new ValueProcessor<Optional<? extends P>>() { |
public void update(Updater updater) { |
setCurrentValue(calculate()); |
updater.reportChanged(this); |
} |
- private Optional<P> calculate() { |
+ private Optional<? extends P> calculate() { |
Optional<? extends T> control = sourceForMerge.getValue(); |
if (control.isNormal()) { |
- ValueSource<? extends Optional<P>> oneSource = map.get(control.getNormal()); |
+ ValueSource<? extends Optional<? extends P>> oneSource = map.get(control.getNormal()); |
return oneSource.getValue(); |
} else { |
return createErrorOptional(control.errorMessages()); |
@@ -952,7 +1162,8 @@ public class DialogUtils { |
this.updater = updater; |
} |
- public <P> OptionalSwitcher<P> addOptionalSwitch(Gettable<? extends Optional<P>> expression) { |
+ public <P> OptionalSwitcher<P> addOptionalSwitch( |
+ Gettable<? extends Optional<? extends P>> expression) { |
OptionalSwitcherImpl<P> switcher = new OptionalSwitcherImpl<P>(this, expression); |
updater.addConsumer(this, switcher.getValueConsumer()); |
updater.addSource(this, switcher.getSourceForMerge()); |