blob: 6dc25ed01c42ae07e9b9a6da5ad605986c3887c9 [file] [log] [blame]
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// This program and the accompanying materials are made available
// under the terms of the Eclipse Public License v2.0 which accompanies
// this distribution, and is available at
// https://www.eclipse.org/legal/epl-2.0/
package org.eclipse.wst.jsdt.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();
}