// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 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.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jface.dialogs.IMessageProvider; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; /** * A small set of utility classes that help programming dialog window logic. */ public class DialogUtils { /* * Part 1. Update graph. * * All logical elements of dialog and dependencies between them are modeled as DAG. * The data flows from vertices that corresponds to various input fields through * some transformations to the terminal vertices that are in-dialog helper messages and OK button. * * A linear data flow (no forks, no merges) is suitable for data transformation and * is programmed manually. Forks and merges are hard to dispatch manually and are managed by * class Updater. * * Updater knows about "source" vertices that may have several outgoing edges and about * "consumer" vertices that may have several incoming edges. Based on source vertex changes * updater updates consumer vertices in topological order. */ /** * A basic interface for anything that can give a value. */ public interface Gettable<RES> { RES getValue(); } /** * Represents source vertex for Updater. Technically updater uses this interface only as a flag * interface, because the only methods it uses are {@link Object#equals}/Object{@link #hashCode}. */ public interface ValueSource<T> extends Gettable<T> { /** * Method is not used by updater, for convenience only. * @return current value of the vertex */ T getValue(); } /** * Represents consumer vertex for Updater. Each instance should be explicitly registered in * Updater. */ public interface ValueConsumer { /** * Called by updater when some linked sources have changed and it's time this vertex * got updated. {@link Updater#reportChanged} may be called if some {@line ValueSource}s have * changed during this update (but this should not break topological order of the graph). */ void update(Updater updater); } /** * Helps to conduct update for vertices in a value graph. * Technically Updater does not see a real graph, because it doesn't support vertices * that are simultaneously source and consumer. Programmer helps manage other edges manually by * calling {@link #reportChanged} method. * <p> * Updater supports conditional updating. At some place in graph there may be switcher that * introduces several scopes. The switcher has only one active scope at a time, that is * controlled by an expression provided. The scope may contain parts of the graph and only * the active scope has its vertices updated, update in other scopes is deferred. Vertices from * the scope are only available via vertex called merger from outside of this scope. * Scopes may be nested. Updater always has a root scope. */ public static class Updater { private final LinkedHashMap<ValueConsumer, Boolean> needsUpdateMap = new LinkedHashMap<ValueConsumer, Boolean>(); private final Map<ValueSource<?>, List<ValueConsumer>> reversedDependenciesMap = new HashMap<ValueSource<?>, List<ValueConsumer>>(); private final Map<ValueConsumer, ScopeImpl> consumer2Scope = new HashMap<ValueConsumer, ScopeImpl>(); private final Map<ValueSource<?>, ScopeImpl> source2Scope = new HashMap<ValueSource<?>, ScopeImpl>(); private boolean alreadyUpdating = false; private volatile boolean asyncStopped = false; private final ScopeImpl rootScope = new ScopeImpl(this); public void addConsumer(ValueConsumer value, ValueSource<?> ... dependencies) { addConsumer(value, Arrays.asList(dependencies)); } /** * Registers a consumer vertex with all its dependencies. The root scope is assumed. */ public void addConsumer(ValueConsumer value, List<? extends ValueSource<?>> dependencies) { addConsumer(rootScope, value); for (ValueSource<?> dep : dependencies) { addDependency(value, dep); } } /** * Registers a consumer within a particular scope. */ public void addConsumer(Scope scope, ValueConsumer consumer) { Boolean res = needsUpdateMap.put(consumer, Boolean.FALSE); if (res != null) { throw new IllegalArgumentException("Already added"); //$NON-NLS-1$ } consumer2Scope.put(consumer, (ScopeImpl)scope); } /** * Registers a source within a particular scope. */ public void addSource(Scope scope, ValueSource<?> source) { ScopeImpl scopeImpl = (ScopeImpl) scope; Object conflict = source2Scope.put(source, scopeImpl); if (conflict != null) { throw new IllegalArgumentException("Already added"); //$NON-NLS-1$ } } /** * Adds a dependency of consumer on source. Source may be from the same scope or from the outer * scope. */ public void addDependency(ValueConsumer consumer, ValueSource<?> source) { Scope consumerScope = consumer2Scope.get(consumer); if (consumerScope == null) { throw new IllegalArgumentException("Unregistered consumer"); //$NON-NLS-1$ } checkSourceVisibleInScope(source, consumerScope); addDependencyNoCheck(consumer, source); } /** * Adds a dependency of consumer on several sources. * See {@link #addDependency(ValueConsumer, ValueSource)}. */ public void addDependency(ValueConsumer consumer, List<? extends ValueSource<?>> sourceList) { for (ValueSource<?> source : sourceList) { addDependency(consumer, source); } } /** * Reports about sources that have been changed and plans future update of consumers. This * method may be called at any time. */ public synchronized void reportChanged(ValueSource<?> source) { List<ValueConsumer> reversedDeps = reversedDependenciesMap.get(source); if (reversedDeps != null) { for (ValueConsumer consumer : reversedDeps) { addConsumerToUpdate(consumer); } } } /** * Performs update of all vertices that need it. If some sources are reported changed * during the run of this method, their consumers are also updated. */ public void update() { if (alreadyUpdating) { return; } alreadyUpdating = true; try { updateImpl(); } finally { alreadyUpdating = false; } } /** * Updates all consumer vertices in graph. */ public void updateAll() { for (Map.Entry<?, Boolean> en : needsUpdateMap.entrySet()) { en.setValue(Boolean.TRUE); } update(); rootScope.processDisabledNested(); } /** * Returns a root scope that updater always has. */ public Scope rootScope() { return rootScope; } /** * Request deferred update. This method may be called from any thread. */ public void updateAsync() { Display.getDefault().asyncExec(new Runnable() { public void run() { if (asyncStopped) { return; } update(); } }); } /** * Stops asynchronous updates -- an activity which is hard to stop directly. */ public void stopAsync() { asyncStopped = true; } void addDependencyNoCheck(ValueConsumer value, ValueSource<?> source) { List<ValueConsumer> reversedDeps = reversedDependenciesMap.get(source); if (reversedDeps == null) { reversedDeps = new ArrayList<ValueConsumer>(2); reversedDependenciesMap.put(source, reversedDeps); } reversedDeps.add(value); } /** * Makes sure that source is from the scope or from its ancestor. */ void checkSourceVisibleInScope(ValueSource<?> source, Scope scope) { Scope sourceScope = source2Scope.get(source); if (sourceScope == null) { throw new IllegalArgumentException("Unregistered source"); //$NON-NLS-1$ } do { if (sourceScope.equals(scope)) { return; } scope = scope.getOuterScope(); } while (scope != null); throw new RuntimeException("Source from a wrong scope"); //$NON-NLS-1$ } void addConsumerToUpdate(ValueConsumer consumer) { needsUpdateMap.put(consumer, Boolean.TRUE); } private void updateImpl() { boolean hasChanges = true; while (hasChanges) { hasChanges = false; for (Map.Entry<ValueConsumer, Boolean> en : needsUpdateMap.entrySet()) { if (en.getValue() == Boolean.TRUE) { en.setValue(Boolean.FALSE); ValueConsumer currentValue = en.getKey(); ScopeImpl scope = consumer2Scope.get(currentValue); if (scope.isActive()) { currentValue.update(this); } else { scope.addDelayedConsumer(currentValue); } } } } } } /** * A basic implementation of object that is both consumer and source. Updater will treat * as 2 separate objects. */ public static abstract class ValueProcessor<T> implements ValueConsumer, ValueSource<T> { private T currentValue = null; public T getValue() { return currentValue; } protected void setCurrentValue(T currentValue) { this.currentValue = currentValue; } } /** * A scope for update graph vertices. Scope may have inner switchers, that own other (nested) * scopes. * <p>Scope roughly corresponds to a group of UI controls that may become disabled, * and thus may not generate any data and need no inner updates. */ public interface Scope { Scope getOuterScope(); /** * Creates a switcher that is operated by optional expression. * @param <T> type of expression */ <T> OptionalSwitcher<T> addOptionalSwitch(Gettable<? extends Optional<? extends T>> expression); /** * Creates a switcher that is operated by non-optional expression. * @param <T> type of expression */ <T> Switcher<T> addSwitch(Gettable<T> expression); } /** * A callback that lets UI to reflect that some scope became enabled/disabled. */ public interface ScopeEnabler { void setEnabled(boolean enabled, boolean recursive); } /** * Base interface for 2 types of switchers. The switcher is a logical element that * enables/disables its scopes according to the value of its expression. * @param <T> type of expression */ public interface SwitchBase<T> { Scope addScope(T tag, ScopeEnabler scopeEnabler); ValueConsumer getValueConsumer(); Updater getUpdater(); Scope getOuterScope(); } /** * A switcher that is operated by non-optional expression. */ public interface Switcher<T> extends SwitchBase<T> { /** * Creates a merge element, that links to sources inside switcher's scopes and * exposes them outside a single {@link ValueSource} that has a value of a corresponding source * in an active scope. * @param sources the list of sources in the corresponding scopes; must be in the same order */ <P> ValueSource<P> createMerge(ValueSource<? extends P> ... sources); } /** * A switcher that is operated by optional expression. */ public interface OptionalSwitcher<T> extends SwitchBase<T> { /** * See javadoc for {@link Switcher#createMerge}; the difference of this method is that * 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<? extends P>> createOptionalMerge( ValueSource<? extends Optional<? extends P>> ... sources); } public static <T> ValueSource<T> createConstant(final T constnant, Updater updater) { ValueSource<T> source = new ValueSource<T>() { public T getValue() { return constnant; } }; updater.addSource(updater.rootScope(), source); return source; } /* * Part 2. Optional data type etc * * Since dialog should deal with error user entry, the typical data type is either value or error. * This is implemented as Optional interface. Most of data transformations should work only * when all inputs are non-error and generate error in return otherwise. This is implemented in * ExpressionProcessor. */ /** * A primitive approach to "optional" algebraic type. This type is T + Set<Message>. */ public interface Optional<V> { V getNormal(); boolean isNormal(); Set<? extends Message> errorMessages(); } public static <V> Optional<V> createOptional(final V value) { return new Optional<V>() { public Set<Message> errorMessages() { return Collections.emptySet(); } public V getNormal() { return value; } 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(); } }; } public static <V> Optional<V> createErrorOptional(Message message) { return createErrorOptional(Collections.singleton(message)); } public static <V> Optional<V> createErrorOptional(final Set<? extends Message> messages) { return new Optional<V>() { public Set<? extends Message> errorMessages() { return messages; } public V getNormal() { throw new UnsupportedOperationException(); } 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(); } }; } /** * A user interface message for dialog window. It has text and priority that helps choosing * the most important message it there are many of them. */ public static class Message { private final String text; private final MessagePriority priority; public Message(String text, MessagePriority priority) { this.text = text; this.priority = priority; } public String getText() { return text; } public MessagePriority getPriority() { return priority; } } /** * Priority of a user interface message. * Constants are listed from most important to least important. */ public enum MessagePriority { BLOCKING_PROBLEM(IMessageProvider.ERROR), BLOCKING_INFO(IMessageProvider.NONE), WARNING(IMessageProvider.WARNING), NONE(IMessageProvider.NONE); private final int messageProviderType; private MessagePriority(int messageProviderType) { this.messageProviderType = messageProviderType; } public int getMessageProviderType() { return messageProviderType; } } /** * A base class for the source-consumer pair that accepts several values as a consumer, * performs a calculation over them and gives it away the result via source interface. * Some sources may be of Optional type. If any of sources has error value the corresponding * error value is returned automatically. * <p> * The implementation should override a single method {@link #calculateNormal}. */ public static abstract class ExpressionProcessor<T> extends ValueProcessor<Optional<T>> { private final List<ValueSource<? extends Optional<?>>> optionalSources; public ExpressionProcessor(List<ValueSource<? extends Optional<?>>> optionalSources) { this.optionalSources = optionalSources; } protected abstract Optional<T> calculateNormal(); private Optional<T> calculateNewValue() { Set<Message> errors = new LinkedHashSet<Message>(0); for (ValueSource<? extends Optional<?>> source : optionalSources) { if (!source.getValue().isNormal()) { errors.addAll(source.getValue().errorMessages()); } } if (errors.isEmpty()) { return calculateNormal(); } else { return createErrorOptional(errors); } } public void update(Updater updater) { Optional<T> result = calculateNewValue(); Optional<T> oldValue = getValue(); setCurrentValue(result); if (!result.equals(oldValue)) { updater.reportChanged(this); } } } /** * 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<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 { } /** * 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 { } } /** * 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<? extends RES>> handleErrors( final NormalExpression<RES> expression) { return NORMAL_EXPRESSION_WRAPPER.handleErrors(expression); } public static ValueSource<? extends Optional<?>>[] dependencies( ValueSource<? extends Optional<?>>... sources) { return sources; } /** * Annotates variable getter methods in interfaces that are used in * {@link #mergeBranchVariables} methods. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface BranchVariableGetter {} /** * For output variables from several branches of switch creates merged variables that * could be used outside of the switch. Variables are provided and returned in form * of interface VARIABLES. * @param <VARIABLES> an interface that should consist only of no-param getters with * {@link BranchVariableGetter} annotation that return ValueSource<...> types; * must not be a generic type * @param switcher the variables get merged from * @param branches output variables of all switch branches; should be in the same * order branches (aka scopes) are added to the switch * @return variable sources that could be used outside switch branches */ public static <VARIABLES> VARIABLES mergeBranchVariables(Class<VARIABLES> variablesType, Switcher<?> switcher, VARIABLES ... branches) { return VARIABLE_MERGER_CACHE.mergeBranches(variablesType, switcher, branches); } /** * For output variables from several branches of switch creates merged variables that * could be used outside of the switch. Variables are provided and returned in form * of interface VARIABLES. * @param <VARIABLES> an interface that should consist only of no-param getters with * {@link BranchVariableGetter} annotation that return * ValueSource< [? extends] Optional < ... > > types; must not be a generic type * @param switcher the variables get merged from * @param branches output variables of all switch branches; should be in the same * order branches (aka scopes) are added to the switch * @return variable sources that could be used outside switch branches */ public static <VARIABLES> VARIABLES mergeBranchVariables(Class<VARIABLES> variablesType, OptionalSwitcher<?> switcher, VARIABLES ... branches) { return OPTIONAL_VARIABLE_MERGER_CACHE.mergeBranches(variablesType, switcher, branches); } /* * Part 3. Various utils. */ /** * A general-purpose implementation of OK button vertex. It works as a consumer of * 1 result value and several warning sources. From its sources it decides whether * OK button should be enabled and also provides dialog messages (errors, warnings, infos). */ public static class OkButtonControl<T> implements ValueConsumer { private final ValueSource<? extends Optional<? extends T>> resultSource; private final List<? extends ValueSource<String>> warningSources; private final OkButtonElements dialogElements; public OkButtonControl(ValueSource<? extends Optional<? extends T>> resultSource, List<? extends ValueSource<String>> warningSources, OkButtonElements dialogElements) { this.resultSource = resultSource; this.warningSources = warningSources; this.dialogElements = dialogElements; } /** * Returns a list of dependencies for updater -- a convenience method. */ public List<? extends ValueSource<?>> getDependencies() { ArrayList<ValueSource<?>> result = new ArrayList<ValueSource<?>>(); result.add(resultSource); result.addAll(warningSources); return result; } public void update(Updater updater) { Optional<?> result = resultSource.getValue(); List<Message> messages = new ArrayList<Message>(); for (ValueSource<String> warningSource : warningSources) { String warningValue = warningSource.getValue(); if (warningValue != null) { messages.add(new Message(warningValue, MessagePriority.WARNING)); } } boolean enabled; if (result.isNormal()) { enabled = true; } else { enabled = false; messages.addAll(result.errorMessages()); } dialogElements.getOkButton().setEnabled(enabled); Message visibleMessage = chooseImportantMessage(messages); dialogElements.setMessage(visibleMessage.getText(), visibleMessage.getPriority().getMessageProviderType()); } public T getNormalValue() { Optional<? extends T> optional = resultSource.getValue(); if (optional.isNormal()) { return optional.getNormal(); } else { return null; } } } public static final Message NULL_MESSAGE = new Message(null, MessagePriority.NONE); public static Message chooseImportantMessage(Collection<? extends Message> messages) { if (messages.isEmpty()) { return NULL_MESSAGE; } return Collections.max(messages, messageComparatorBySeverity); } private static final Comparator<Message> messageComparatorBySeverity = new Comparator<Message>() { public int compare(Message o1, Message o2) { int ordinal1 = o1.getPriority().ordinal(); int ordinal2 = o2.getPriority().ordinal(); if (ordinal1 < ordinal2) { return +1; } else if (ordinal1 == ordinal2) { return 0; } else { return -1; } } }; /** * An access to OK button and related elements of the dialog window from dialog logic part. */ public interface OkButtonElements { Button getOkButton(); void setMessage(String message, int type); } /** * A wrapper around Combo that provides logic-level data-oriented access to the control. * This is not a simply convenience wrapper, because Combo itself does not keep a real data, * but only its string representation. */ public static abstract class ComboWrapper<E> { private final Combo combo; public ComboWrapper(Combo combo) { this.combo = combo; } public Combo getCombo() { return combo; } public void addSelectionListener(SelectionListener listener) { combo.addSelectionListener(listener); } public abstract E getSelected(); public abstract void setSelected(E element); } public static <T> ValueProcessor<T> createProcessor(final Gettable<T> expression) { return new ValueProcessor<T>() { public void update(Updater updater) { T newValue = expression.getValue(); T oldValue = getValue(); boolean same = (newValue == null) ? oldValue == null : newValue.equals(oldValue); setCurrentValue(newValue); if (!same) { updater.reportChanged(this); } } }; } /** * Adds a standard modify listener to a text element that binds it to updater. */ public static void addModifyListener(Text textElement, final ValueSource<?> valueSource, final Updater updater) { ModifyListener listener = new ModifyListener() { public void modifyText(ModifyEvent e) { updater.reportChanged(valueSource); updater.update(); } }; textElement.addModifyListener(listener); } /** * Adds a standard modify listener to a button that binds it to updater. */ public static void addModifyListener(Button button, final ValueSource<?> valueSource, final Updater updater) { SelectionListener listener = new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { updater.reportChanged(valueSource); updater.update(); } @Override public void widgetDefaultSelected(SelectionEvent e) { updater.reportChanged(valueSource); updater.update(); } }; button.addSelectionListener(listener); } /* * Part 4. Implementation stuff. */ private static abstract class SwitcherBaseImpl<T> implements SwitchBase<T> { private final ScopeImpl outerScope; private final Map<T, ScopeImpl> innerScopes = new LinkedHashMap<T, ScopeImpl>(2); private ScopeImpl currentScope = null; private final ValueConsumer consumer = new ValueConsumer() { public void update(Updater updater) { updateScopes(); updater.reportChanged(getSourceForMerge()); } }; ScopeImpl getScopeForTag(T tag) { return innerScopes.get(tag); } abstract void updateScopes(); abstract ValueSource<?> getSourceForMerge(); void setCurrentScope(ScopeImpl newScope) { if (newScope == currentScope) { return; } if (currentScope != null) { currentScope.setEnabled(false); } currentScope = newScope; if (currentScope != null) { currentScope.setEnabled(true); } } ScopeImpl getCurrentScope() { return currentScope; } public Updater getUpdater() { return outerScope.getUpdater(); } SwitcherBaseImpl(ScopeImpl outerScope) { this.outerScope = outerScope; this.outerScope.nestedSwitchers.add(this); } public Scope addScope(T tag, ScopeEnabler scopeEnabler) { ScopeImpl scope = new ScopeImpl(this, scopeEnabler, outerScope.getUpdater()); Object conflict = innerScopes.put(tag, scope); if (conflict != null) { throw new IllegalStateException(); } return scope; } public ValueConsumer getValueConsumer() { return consumer; } public ScopeImpl getOuterScope() { return outerScope; } <V extends ValueSource<?>> Map<T,V> sortSources(List<V> sources) { if (innerScopes.size() != sources.size()) { throw new IllegalArgumentException(); } Map<T,V> result = new HashMap<T, V>(); Updater updater = outerScope.getUpdater(); int i = 0; for (Map.Entry<T, ScopeImpl> en : innerScopes.entrySet()) { V source = sources.get(i); updater.checkSourceVisibleInScope(source, en.getValue()); result.put(en.getKey(), source); i++; } return result; } void processDisabledScopes() { for (ScopeImpl scope : innerScopes.values()) { if (scope != currentScope) { if (scope.scopeEnabler != null) { scope.scopeEnabler.setEnabled(false, false); } } scope.processDisabledNested(); } } } private static class SwitcherImpl<T> extends SwitcherBaseImpl<T> implements Switcher<T> { private final Gettable<? extends T> expression; private final ValueSource<T> sourceForMerge = new ValueSource<T>() { public T getValue() { return expression.getValue(); } }; SwitcherImpl(ScopeImpl outerScope, Gettable<T> expression) { super(outerScope); this.expression = expression; } @Override ValueSource<?> getSourceForMerge() { return sourceForMerge; } @Override void updateScopes() { T tag = expression.getValue(); ScopeImpl newScope = getScopeForTag(tag); setCurrentScope(newScope); } public <P> ValueSource<P> createMerge(ValueSource<? extends P> ... sources) { final Map<T, ValueSource<? extends P>> map = sortSources(Arrays.asList(sources)); ValueProcessor<P> result = new ValueProcessor<P>() { public void update(Updater updater) { setCurrentValue(calculate()); updater.reportChanged(this); } private P calculate() { T tag = sourceForMerge.getValue(); ValueSource<? extends P> oneSource = map.get(tag); return oneSource.getValue(); } }; ScopeImpl outerScope = getOuterScope(); Updater updater = outerScope.getUpdater(); updater.addConsumer(outerScope, result); updater.addDependency(result, getSourceForMerge()); for (ValueSource<?> source : sources) { updater.addDependencyNoCheck(result, source); // AddNoCheck } outerScope.getUpdater().addSource(outerScope, result); return result; } } private static class OptionalSwitcherImpl<T> extends SwitcherBaseImpl<T> implements OptionalSwitcher<T> { private final Gettable<? extends Optional<? extends T>> expression; private final ValueSource<Optional<? extends T>> sourceForMerge = new ValueSource<Optional<? extends T>>() { public Optional<? extends T> getValue() { return expression.getValue(); } }; OptionalSwitcherImpl(ScopeImpl outerScope, Gettable<? extends Optional<? extends T>> expression) { super(outerScope); this.expression = expression; } @Override ValueSource<?> getSourceForMerge() { return sourceForMerge; } @Override void updateScopes() { ScopeImpl newScope; Optional<? extends T> control = expression.getValue(); if (control.isNormal()) { T tag = control.getNormal(); newScope = getScopeForTag(tag); } else { newScope = null; } setCurrentScope(newScope); } public <P> ValueSource<? extends Optional<? extends P>> createOptionalMerge( ValueSource<? extends Optional<? extends P>>... sources) { final Map<T, ValueSource<? extends Optional<? extends P>>> map = sortSources(Arrays.asList(sources)); ValueProcessor<? extends Optional<? extends P>> result = new ValueProcessor<Optional<? extends P>>() { public void update(Updater updater) { setCurrentValue(calculate()); updater.reportChanged(this); } private Optional<? extends P> calculate() { Optional<? extends T> control = sourceForMerge.getValue(); if (control.isNormal()) { ValueSource<? extends Optional<? extends P>> oneSource = map.get(control.getNormal()); return oneSource.getValue(); } else { return createErrorOptional(control.errorMessages()); } } }; ScopeImpl outerScope = getOuterScope(); Updater updater = outerScope.getUpdater(); updater.addConsumer(outerScope, result); updater.addDependency(result, getSourceForMerge()); for (ValueSource<?> source : sources) { updater.addDependencyNoCheck(result, source); // AddNoCheck } outerScope.getUpdater().addSource(outerScope, result); return result; } } private static class ScopeImpl implements Scope { private final Updater updater; private final SwitcherBaseImpl<?> switcher; private final ScopeEnabler scopeEnabler; private final Set<ValueConsumer> delayedConsumers = new HashSet<ValueConsumer>(0); private final List<SwitcherBaseImpl<?>> nestedSwitchers = new ArrayList<SwitcherBaseImpl<?>>(1); ScopeImpl(Updater updater) { this(null, null, updater); } public boolean isActive() { if (switcher == null) { return true; } return switcher.getOuterScope().isActive() && switcher.getCurrentScope() == this; } public void addDelayedConsumer(ValueConsumer consumer) { delayedConsumers.add(consumer); } public void setEnabled(boolean enabled) { if (enabled) { for (ValueConsumer consumer : delayedConsumers) { updater.addConsumerToUpdate(consumer); } delayedConsumers.clear(); } if (scopeEnabler != null) { scopeEnabler.setEnabled(enabled, false); } } Updater getUpdater() { return updater; } ScopeImpl(SwitcherBaseImpl<?> switcher, ScopeEnabler scopeEnabler, Updater updater) { this.switcher = switcher; this.scopeEnabler = scopeEnabler; this.updater = updater; } 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()); updater.addDependency(switcher.getValueConsumer(), switcher.getSourceForMerge()); return switcher; } public <P> Switcher<P> addSwitch(Gettable<P> expression) { SwitcherImpl<P> switcher = new SwitcherImpl<P>(this, expression); updater.addConsumer(this, switcher.getValueConsumer()); updater.addSource(this, switcher.getSourceForMerge()); updater.addDependency(switcher.getValueConsumer(), switcher.getSourceForMerge()); return switcher; } public Scope getOuterScope() { if (switcher == null) { return null; } return switcher.getOuterScope(); } void processDisabledNested() { for (SwitcherBaseImpl<?> switcher : nestedSwitchers) { switcher.processDisabledScopes(); } } } private static NormalExpressionWrapper NORMAL_EXPRESSION_WRAPPER = new NormalExpressionWrapper(); private static class NormalExpressionWrapper { private final Map<Class<?>, GettableFactory<?>> classToFactoryMap = new HashMap<Class<?>, GettableFactory<?>>(); <RES> Gettable<Optional<? extends RES>> handleErrors( NormalExpression<RES> expression) { return getFactoryForExpression(expression).create(expression); } private <RES> GettableFactory<RES> getFactoryForExpression(NormalExpression<RES> expression) { Class<? extends NormalExpression> expressionClass = expression.getClass(); GettableFactory<?> factory = classToFactoryMap.get(expressionClass); if (factory == null) { factory = createFactory(expressionClass); classToFactoryMap.put(expressionClass, factory); } // This should be safe, we created factory by this class. return (GettableFactory<RES>) factory; } /** * This method is static and needs a class only. Virtually I may be called even * on build time (e.g. to check that the class implementating {@link NormalExpression} * is consistent). */ private static <RES> GettableFactory<RES> createFactory( Class<? extends NormalExpression> expressionClass) { // All reflection checks are 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; } if (normalExpressionType == null) { throw new IllegalArgumentException("Expression does not directly implement " + NormalExpression.class.getName()); } expressionType = normalExpressionType.getActualTypeArguments()[0]; } // Read all methods of expression class and choose annotated ones. Method calculateMethod = null; final List<Method> dependencyMethods = new ArrayList<Method>(2); for (Method m : expressionClass.getMethods()) { if (m.getAnnotation(NormalExpression.Calculate.class) != null) { if (calculateMethod != null) { throw new IllegalArgumentException("Class " + expressionClass.getName() + " has more than one method with " + NormalExpression.Calculate.class.getName() + " annotation"); } calculateMethod = m; } if (m.getAnnotation(NormalExpression.DependencyGetter.class) != null) { dependencyMethods.add(m); } } if (calculateMethod == null) { throw new IllegalArgumentException("Failed to found Class method with " + NormalExpression.Calculate.class.getName() + " annotation in " + expressionClass.getName()); } 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). final ReturnValueHandler<RES> returnValueHandler; if (methodReturnType.equals(expressionType)) { returnValueHandler = new ReturnValueHandler<RES>() { 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<RES>() { 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); } } return new GettableFactory<RES>(dependencyMethods, returnValueHandler, calculateMethod); } private static class GettableFactory<RES> { private final List<Method> dependencyMethods; private final ReturnValueHandler<RES> returnValueHandler; private final Method calculateMethod; GettableFactory(List<Method> dependencyMethods, ReturnValueHandler<RES> returnValueHandler, Method calculateMethod) { this.dependencyMethods = dependencyMethods; this.returnValueHandler = returnValueHandler; this.calculateMethod = calculateMethod; } Gettable<Optional<? extends RES>> create(final NormalExpression<RES> expression) { // 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()); } } 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 createErrorOptional(errors); } } }; } } private static abstract class ReturnValueHandler<T> { abstract Optional<? extends T> castResult(Object resultObject); } } /** * A reflection-based merger that merges output variables * via {@link Switcher#createMerge(ValueSource...)} and * {@link OptionalSwitcher#createOptionalMerge(ValueSource...)} methods. * @param <SW> particular type of switcher (either simple or optional) */ private static abstract class ScopeMergerBase<SW extends SwitchBase<?>> { private final Map<Class<?>, Factory<SW, ?>> cache = new HashMap<Class<?>, Factory<SW, ?>>(); <VARIABLES> VARIABLES mergeBranches(Class<VARIABLES> variablesType, SW switcher, VARIABLES ... branches) { Factory<SW, VARIABLES> factory = (Factory<SW, VARIABLES>) cache.get(variablesType); if (factory == null) { factory = createFactory(variablesType); cache.put(variablesType, factory); } try { return factory.create(switcher, branches); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (SecurityException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } private interface Factory<S, V> { V create(S switcher, V ... branches) throws IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException; } /** * Analyzes class via reflection and prepares dynamic proxy structure for future instantiation. */ private <VARIABLES> Factory<SW, VARIABLES> createFactory( final Class<VARIABLES> variablesType) { if (variablesType.getTypeParameters().length != 0) { // It's more difficult to implement parameterized types. // We have to make sure that parameter types are the same for all instances. throw new IllegalArgumentException("Type should not be parameterized"); } final List<Method> getterMethods = new ArrayList<Method>(); final Map<Method, Integer> methodToPosition = new HashMap<Method, Integer>(); for (Method m : variablesType.getDeclaredMethods()) { if (m.getAnnotation(BranchVariableGetter.class) == null) { throw new IllegalArgumentException("Method "+ m + " should have " + BranchVariableGetter.class.getName() + " annotation"); } if (m.getParameterTypes().length != 0) { throw new IllegalArgumentException("Method "+ m + " should have no parameters"); } Type returnType = m.getGenericReturnType(); try { if (returnType instanceof ParameterizedType == false) { throw new IllegalArgumentException("Method should return parameterized type " + ValueSource.class); } ParameterizedType parameterizedType = (ParameterizedType) returnType; if (parameterizedType.getRawType() != ValueSource.class) { throw new IllegalArgumentException("Method should return parameterized type " + ValueSource.class); } checkGetterType(parameterizedType.getActualTypeArguments()[0]); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Method "+ m + " has wrong return type", e); } int position = getterMethods.size(); getterMethods.add(m); methodToPosition.put(m, position); } Class<?> proxyClass = Proxy.getProxyClass(this.getClass().getClassLoader(), variablesType); final Constructor<?> constructor; try { constructor = proxyClass.getConstructor(InvocationHandler.class); } catch (SecurityException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } return new Factory<SW, VARIABLES>() { @Override public VARIABLES create(SW switcher, VARIABLES... branches) throws IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException { final ValueSource<?> [] dataArray = new ValueSource<?>[getterMethods.size()]; for (int i = 0; i < dataArray.length; i++) { ValueSource<?> [] allBranchesSources = new ValueSource<?>[branches.length]; Method interfaceGetter = getterMethods.get(i); for (int j = 0; j < allBranchesSources.length; j++) { if (branches[j] == null) { allBranchesSources[j] = createConstant(null, switcher.getUpdater()); } else { Method classGetter = branches[j].getClass().getMethod(interfaceGetter.getName(), interfaceGetter.getParameterTypes()); classGetter.setAccessible(true); allBranchesSources[j] = (ValueSource<?>) classGetter.invoke(branches[j]); } } dataArray[i] = createValueMerger(switcher, allBranchesSources); } InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (OBJECT_METHODS.contains(method)) { return method.invoke(this, args); } Integer position = methodToPosition.get(method); if (position == null) { throw new RuntimeException("Unknown method: " + method); } return dataArray[position]; } @Override public String toString() { return "*Merged:" + variablesType; } }; Object resultInstance; try { resultInstance = constructor.newInstance(invocationHandler); } catch (InstantiationException e) { throw new RuntimeException(e); } return (VARIABLES) resultInstance; } }; } // Switcher-specific method. protected abstract ValueSource<?> createValueMerger(SW switcher, ValueSource<?>[] allCasesSources); protected abstract void checkGetterType(Type valueSourceParamType); private static final Set<Method> OBJECT_METHODS; static { OBJECT_METHODS = new HashSet<Method>(); try { OBJECT_METHODS.add(Object.class.getMethod("toString")); OBJECT_METHODS.add(Object.class.getMethod("equals", Object.class)); OBJECT_METHODS.add(Object.class.getMethod("hashCode")); } catch (SecurityException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } } /** * Merger specialization for simple switcher. */ private static class ScopeMerger extends ScopeMergerBase<Switcher<?>> { protected ValueSource<?> createValueMerger(Switcher<?> switcher, ValueSource<?>[] allCasesSources) { return switcher.createMerge(allCasesSources); } /** * Getter must return ValueSource< ... >. */ @Override protected void checkGetterType(Type valueSourceParamType) { // Nothing to do. } } /** * Merger specialization for optional switcher. */ private static class OptionalScopeMerger extends ScopeMergerBase<OptionalSwitcher<?>> { protected ValueSource<?> createValueMerger(OptionalSwitcher<?> switcher, ValueSource<?>[] allCasesSources) { // It's ok, we check method return type via reflection. ValueSource<? extends Optional<?>>[] castedSources = (ValueSource<? extends Optional<?>>[]) allCasesSources; return switcher.createOptionalMerge(castedSources); } /** * Getter must return ValueSource< [? extends ] Optional<...> >. */ @Override protected void checkGetterType(Type valueSourceParamType) { Type innerParameter; if (valueSourceParamType instanceof WildcardType) { WildcardType wildcardType = (WildcardType) valueSourceParamType; innerParameter = wildcardType.getUpperBounds()[0]; if (innerParameter == null) { throw new IllegalArgumentException("Method should return parameterized type " + ValueSource.class + " with Optional parameter type"); } } else { innerParameter = valueSourceParamType; } Type innerParameterRawType; if (innerParameter instanceof ParameterizedType) { ParameterizedType innerParameterParameterizedType = (ParameterizedType) innerParameter; innerParameterRawType = innerParameterParameterizedType.getRawType(); } else { innerParameterRawType = innerParameter; } if (!innerParameterRawType.equals(Optional.class)) { throw new IllegalArgumentException("Method should return parameterized type " + ValueSource.class + " with Optional parameter type"); } } } private static final ScopeMerger VARIABLE_MERGER_CACHE = new ScopeMerger(); private static final OptionalScopeMerger OPTIONAL_VARIABLE_MERGER_CACHE = new OptionalScopeMerger(); }