package org.xpect.state;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.xtext.util.IAcceptor;
import org.eclipse.xtext.xbase.lib.Exceptions;
import org.xpect.parameter.ParameterParser;
import org.xpect.state.ResolvedConfiguration.DerivedValue;
import org.xpect.state.ResolvedConfiguration.Factory;
import org.xpect.state.ResolvedConfiguration.PrimaryValue;
import org.xpect.state.ResolvedConfiguration.Value;
import org.xpect.text.CharSequences;
import org.xpect.text.Table;
import org.xpect.text.Table.Row;
import com.google.common.base.Joiner;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Similar to a Guice injector. It is created from the {@link Configuration} by the Xpect runners. It can inject using key (annotation) and type (of object). If the type is unique,
* the default annotation is used and only the type is used as a key. This injector is aware of dependencies, that is, if an object is to be created which needs other objects,
* these other objects are created as well.
* <p>
* The state container is also used to retrieve arguments for a test method call, see {@link #tryGet(Class, Object...)} for details.
*
*/
public class StateContainer {
protected class FactoryInstance {
private final List<Instance> down = Lists.newArrayList();
private final List<Instance> up = Lists.newArrayList();
private final Factory factory;
private final Object instance;
public FactoryInstance(Factory factory, Object instance) {
super();
this.factory = factory;
this.instance = instance;
}
}
protected abstract class Instance implements Managed<Object> {
private final List<FactoryInstance> down = Lists.newArrayList();
private FactoryInstance up = null;
private final Value key;
public Instance(Value key) {
super();
this.key = key;
}
public void invalidate() {
StateContainer.this.invalidate(this);
}
}
protected class ManagedInstance extends Instance {
private final Managed<?> value;
public ManagedInstance(Value key, Managed<?> value) {
super(key);
this.value = value;
}
public Object get() {
return value.get();
}
}
protected static class State {
private final Map<Factory, FactoryInstance> factory2instance = Maps.newLinkedHashMap();
private final Map<Value, Instance> value2instance = Maps.newLinkedHashMap();
}
protected class ValueInstance extends Instance {
private final Object value;
public ValueInstance(Value key, Object value) {
super(key);
this.value = value;
}
public Object get() {
return value;
}
}
private final ResolvedConfiguration config;
private final State state;
public StateContainer(ResolvedConfiguration config) {
this.config = config;
this.state = new State();
}
public StateContainer(IAcceptor<Configuration> config) {
Configuration cfg = new Configuration("default");
config.accept(cfg);
this.config = new ResolvedConfiguration(cfg);
this.state = new State();
}
public StateContainer(StateContainer parent, ResolvedConfiguration config) {
this.config = config;
this.state = parent.state;
if (parent.config != this.config.getParent())
throw new IllegalStateException("Parent configs must be the same");
}
public StateContainer(StateContainer parent, IAcceptor<Configuration> config) {
Configuration cfg = new Configuration("default");
config.accept(cfg);
this.config = new ResolvedConfiguration(cfg);
this.state = parent.state;
if (parent.config != this.config.getParent())
throw new IllegalStateException("Parent configs must be the same");
}
protected void collectDependees(FactoryInstance fact, Set<FactoryInstance> factories, Set<Instance> instances) {
for (Instance i : fact.down)
if (instances.add(i))
collectDependees(i, factories, instances);
}
protected void collectDependees(Instance instance, Set<FactoryInstance> factories, Set<Instance> instances) {
for (FactoryInstance fi : instance.down)
if (factories.add(fi))
collectDependees(fi, factories, instances);
}
protected FactoryInstance createFactory(Factory factory) {
Constructor<?> constructor = factory.getConstructor();
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] paramsObjects = new Object[paramTypes.length];
Instance[] paramsInstances = new Instance[paramTypes.length];
Annotation[][] paramAnnot = constructor.getParameterAnnotations();
for (int i = 0; i < paramTypes.length; i++) {
paramsInstances[i] = (Instance) get(paramTypes[i], (Object[]) paramAnnot[i]);
paramsObjects[i] = paramsInstances[i].get();
}
try {
Object instance = constructor.newInstance(paramsObjects);
FactoryInstance result = new FactoryInstance(factory, instance);
for (Instance inst : paramsInstances) {
inst.down.add(result);
result.up.add(inst);
}
return result;
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
Exceptions.sneakyThrow(e.getCause());
return null;
}
}
protected Instance createInstance(DerivedValue value) {
FactoryInstance factory = getFactory(value.getFactory());
try {
Object instance = value.getMethod().invoke(factory.instance);
Instance result = createInstance(value, instance);
factory.down.add(result);
result.up = factory;
return result;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
protected Instance createInstance(PrimaryValue value) {
return createInstance(value, value.getValue());
}
protected Instance createInstance(Value value) {
if (value instanceof PrimaryValue)
return createInstance((PrimaryValue) value);
if (value instanceof DerivedValue)
return createInstance((DerivedValue) value);
throw new IllegalStateException();
}
@SuppressWarnings("unchecked")
protected Instance createInstance(Value key, Object value) {
if (value instanceof Managed<?>)
return new ManagedInstance(key, (Managed<Object>) value);
return new ValueInstance(key, value);
}
public <T> Managed<T> get(Class<T> expectedType, Object... annotations) {
return get(expectedType, true, annotations);
}
/**
* The following workflow is performed to get the argument of expected type (annotated with given annotations):
* <ol>
* <li>get "raw" value:
* <ol>
* <li>get the parameter from the parser defined by {@link ParameterParser} if possible
* <li>if this fails, get value identified via annotations (used as key) and type
* </ol>
* <li>for all parsers (some parameters may be parsers, e.g., expectations), get the {@link IParameterProvider} via {@code parseRegion()} defined similarly (not equally) in
* {@link IParameterParser} subtypes.
* <li>if necessary, also create {@link IParameterAdapter}, configured via {@link XpectParameterAdapter}.
* <li>use {@link IParameterProvider} or {@link IParameterAdapter} to get argument
* </ol>
*/
public <T> T tryGet(Class<T> expectedType, Object... annotations) {
Managed<T> managed = get(expectedType, false, annotations);
if (managed != null)
return managed.get();
return null;
}
@SuppressWarnings("unchecked")
protected <T> Managed<T> get(Class<T> expectedType, boolean throwException, Object... annotations) {
if (expectedType == StateContainer.class)
return (Managed<T>) new ManagedInstance(null, new ManagedImpl<T>((T) this));
Class<? extends Annotation> annotatedWith = getAnnotation(annotations);
Value value = config.getValue(annotatedWith, expectedType);
if (value == null) {
if (throwException) {
throw new IllegalStateException("Unknown key @" + annotatedWith.getName() + " " + expectedType.getName());
} else
return null;
}
Instance instance = state.value2instance.get(value);
if (instance == null)
state.value2instance.put(value, instance = createInstance(value));
return (Managed<T>) instance;
}
@SuppressWarnings("unchecked")
protected Class<? extends Annotation> getAnnotation(Object... annotations) {
if (annotations.length == 0)
return Default.class;
Object annotation = annotations[0];
if (annotation instanceof Class<?>)
return (Class<? extends Annotation>) annotation;
if (annotation instanceof Annotation)
return ((Annotation) annotation).annotationType();
throw new IllegalStateException();
}
public ResolvedConfiguration getConfiguration() {
return config;
}
protected FactoryInstance getFactory(Factory factory) {
FactoryInstance instance = state.factory2instance.get(factory);
if (instance == null)
state.factory2instance.put(factory, instance = createFactory(factory));
return instance;
}
public void invalidate() {
Set<FactoryInstance> factories = Sets.newLinkedHashSet();
Set<Instance> instances = Sets.newLinkedHashSet();
for (PrimaryValue fact : config.getPrimaryValues()) {
Instance instance = state.value2instance.get(fact);
if (instance != null && instances.add(instance))
collectDependees(instance, factories, instances);
}
for (Factory fact : config.getResolvedFactories()) {
FactoryInstance instance = state.factory2instance.get(fact);
if (instance != null && factories.add(instance))
collectDependees(instance, factories, instances);
}
invalidate(factories, instances);
}
protected void invalidate(FactoryInstance instance, DerivedValue key, Object val) {
Method invalidator = key.getInvalidator();
try {
invalidator.invoke(instance.instance, val);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
protected void invalidate(Instance instance) {
Set<FactoryInstance> factories = Sets.newLinkedHashSet();
Set<Instance> instances = Sets.newLinkedHashSet();
instances.add(instance);
collectDependees(instance, factories, instances);
invalidate(factories, instances);
}
protected void invalidate(Set<FactoryInstance> factories, Set<Instance> instances) {
for (Instance inst : instances) {
if (inst instanceof ManagedInstance) {
ManagedInstance mi = (ManagedInstance) inst;
mi.value.invalidate();
}
if (inst.key instanceof DerivedValue) {
DerivedValue derivedValue = (DerivedValue) inst.key;
Method invalidator = derivedValue.getInvalidator();
if (invalidator != null) {
FactoryInstance factoryInstance = state.factory2instance.get(derivedValue.getFactory());
invalidate(factoryInstance, derivedValue, inst.get());
}
}
}
for (Instance inst : instances) {
state.value2instance.remove(inst.key);
inst.down.clear();
if (inst.up != null) {
inst.up.down.remove(inst);
}
}
for (FactoryInstance fact : factories) {
state.factory2instance.remove(fact.factory);
fact.down.clear();
for (Instance up : fact.up) {
up.down.remove(fact);
}
}
}
protected class ToString {
protected class ValueRow implements Comparable<ValueRow> {
public ValueRow(ResolvedConfiguration scope, Value value) {
super();
this.declaredScope = scope;
this.value = value;
this.keyType = value.getType() == null ? "null" : value.getType().getName();
this.keyAnnotation = value.getAnnotatedWith().getSimpleName();
}
private final Value value;
private final String keyType;
private final String keyAnnotation;
private final ResolvedConfiguration declaredScope;
private ResolvedConfiguration lifecycleScope = null;
private Set<ValueRow> dependencies = Sets.newHashSet();
public String getInstanceString() {
if (value instanceof PrimaryValue)
return CharSequences.toSingleLineString(((PrimaryValue) value).getValue(), 80);
Instance instance = state.value2instance.get(value);
if (instance == null)
return "(not created)";
Object object = instance.get();
if (object == null)
return "(null)";
return CharSequences.toSingleLineString(object, 80);
}
public int compareTo(ValueRow o) {
return ComparisonChain.start().compare(keyType, o.keyType).compare(keyAnnotation, keyAnnotation).result();
}
public String getKeyString() {
if (Default.class.getSimpleName().equals(keyAnnotation))
return keyType;
return "@" + keyAnnotation + " " + keyType;
}
public Object getDependenciesString() {
List<ValueRow> dependencies = Lists.newArrayList(this.dependencies);
Collections.sort(dependencies);
List<String> result = Lists.newArrayList();
for (ValueRow val : dependencies)
result.add(val.getKeyString());
return Joiner.on(", ").join(result);
}
public Object getScopeString() {
if (lifecycleScope != null)
return lifecycleScope.getName();
return declaredScope.getName();
}
}
private Map<Value, ValueRow> rows = Maps.newHashMap();
private void collectValues(ResolvedConfiguration cfg) {
for (Value value : cfg.getValues().values())
rows.put(value, new ValueRow(cfg, value));
ResolvedConfiguration parent = cfg.getParent();
if (parent != null)
collectValues(parent);
}
private void updateCollectDependencies(ResolvedConfiguration cfg) {
for (ResolvedConfiguration.Factory fac : cfg.getResolvedFactories())
for (Value out : fac.getOut()) {
ValueRow row = rows.get(out);
for (Value in : fac.getIn())
row.dependencies.add(rows.get(in));
}
ResolvedConfiguration parent = cfg.getParent();
if (parent != null)
updateCollectDependencies(parent);
}
private boolean isMoreNarrow(ResolvedConfiguration candidate, ResolvedConfiguration ref) {
if (candidate == null || ref == null || candidate == ref)
return false;
ResolvedConfiguration parent = candidate.getParent();
if (parent == ref)
return true;
return isMoreNarrow(parent, ref);
}
private ResolvedConfiguration updateScope(ValueRow row) {
ResolvedConfiguration result = row.lifecycleScope;
if (result != null)
return result;
result = row.declaredScope;
for (ValueRow dependency : row.dependencies) {
ResolvedConfiguration dep = updateScope(dependency);
if (isMoreNarrow(dep, result))
result = dep;
}
row.lifecycleScope = result;
return result;
}
@Override
public String toString() {
collectValues(config);
updateCollectDependencies(config);
for (ValueRow row1 : rows.values())
updateScope(row1);
List<ValueRow> rows = Lists.newArrayList(this.rows.values());
Collections.sort(rows);
Table table = new Table();
table.setRowSeparatorHeight(0);
Row header = table.addRow();
header.addCell().setText("Key");
header.addCell().setText("Scope");
header.addCell().setText("Value");
header.addCell().setText("Dependencies");
header.getBottomSeparator().setHeight(1).setBackground("-");
for (ValueRow vr : rows) {
Row row = table.addRow();
row.addCell().setText(vr.getKeyString());
row.addCell().setText(vr.getScopeString());
row.addCell().setText(vr.getInstanceString());
row.addCell().setText(vr.getDependenciesString());
}
return table.toString();
}
}
@Override
public String toString() {
return new ToString().toString();
}
}