package eu.stratosphere.util.dag.converter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import eu.stratosphere.util.OneElementList; import eu.stratosphere.util.dag.ConnectionNavigator; import eu.stratosphere.util.reflect.BoundTypeUtil; /** * Converts a directly acyclic graph to another by performing recursive invocation of {@link NodeConverter}s for * specific node types. * * @author Arvid Heise * @param <InputType> * the type of the input node * @param <OutputType> * the type of the output node */ public class GraphConverter<InputType, OutputType> implements NodeConverter<InputType, OutputType> { private final Map<Class<?>, NodeConverterInfo<InputType, OutputType>> converterInfos = new HashMap<Class<?>, NodeConverterInfo<InputType, OutputType>>(); private final List<GraphConversionListener<InputType, OutputType>> conversionListener = new ArrayList<GraphConversionListener<InputType, OutputType>>(); private final boolean flattenCollection = true; @SuppressWarnings("unchecked") private List<OutputType> lastChildren = Collections.EMPTY_LIST; /** * Adds a {@link GraphConversionListener} that is invoked before and after a specific node is converted. * * @param listener * the listener to add */ public void addListener(final GraphConversionListener<InputType, OutputType> listener) { this.conversionListener.add(listener); } @SuppressWarnings("unchecked") private List<OutputType> convertChildren(final ConnectionNavigator<InputType> navigator, final InputType root, final NodeConverterInfo<InputType, OutputType> converterInfo) { final List<OutputType> childTypes = new ArrayList<OutputType>(); if (converterInfo == null || !converterInfo.isStopRecursion()) for (final InputType child : navigator.getConnectedNodes(root)) { final OutputType handledResult = this.convertGraph(child, navigator); if (this.flattenCollection && handledResult instanceof Collection<?>) childTypes.addAll((Collection<? extends OutputType>) handledResult); else if (handledResult != null) childTypes.add(handledResult); } childTypes.addAll(this.lastChildren); return childTypes; } /** * Converts a graph given by the start node and the referenced nodes reachable with the {@link Navigator}.<br> * For each node the registered {@link NodeConverter} is applied recursively until every reachable node has been * converted.<br> * If a node without appropriate {@link NodeConverter} appears or the converter return null, the first converted * child is returned instead. * * @param startNode * the start node * @param navigator * the navigator * @return the converted start node */ public OutputType convertGraph(final InputType startNode, final ConnectionNavigator<InputType> navigator) { return this.convertGraph(navigator, startNode, new IdentityHashMap<InputType, OutputType>()); } /** * Converts a graph given by the start nodes and the referenced nodes reachable with the {@link Navigator}.<br> * For each node the registered {@link NodeConverter} is applied recursively until every reachable node has been * converted.<br> * If a node without appropriate {@link NodeConverter} appears or the converter return null, the first converted * child is returned instead. * * @param startNodes * the start nodes * @param navigator * the navigator * @return the converted start nodes */ public List<OutputType> convertGraph(final InputType[] startNodes, final ConnectionNavigator<InputType> navigator) { return this.convertGraph(Arrays.asList(startNodes).iterator(), navigator); } /** * Converts a graph given by the start nodes and the referenced nodes reachable with the {@link Navigator}.<br> * For each node the registered {@link NodeConverter} is applied recursively until every reachable node has been * converted.<br> * If a node without appropriate {@link NodeConverter} appears or the converter return null, the first converted * child is returned instead. * * @param startNodes * the start nodes * @param navigator * the navigator * @return the converted start nodes */ public List<OutputType> convertGraph(final Iterable<InputType> startNodes, final ConnectionNavigator<InputType> navigator) { return this.convertGraph(startNodes.iterator(), navigator); } /** * Converts a graph given by the start nodes and the referenced nodes reachable with the {@link Navigator}.<br> * For each node the registered {@link NodeConverter} is applied recursively until every reachable node has been * converted.<br> * If a node without appropriate {@link NodeConverter} appears or the converter return null, the first converted * child is returned instead. * * @param startNodes * the start nodes * @param navigator * the navigator * @return the converted start nodes */ public List<OutputType> convertGraph(final Iterator<InputType> startNodes, final ConnectionNavigator<InputType> navigator) { final List<OutputType> results = new ArrayList<OutputType>(); final IdentityHashMap<InputType, OutputType> convertedNodes = new IdentityHashMap<InputType, OutputType>(); while (startNodes.hasNext()) results.add(this.convertGraph(navigator, startNodes.next(), convertedNodes)); return results; } @SuppressWarnings("unchecked") private OutputType convertGraph(final ConnectionNavigator<InputType> navigator, final InputType root, final IdentityHashMap<InputType, OutputType> convertedNodes) { if (convertedNodes.containsKey(root)) return convertedNodes.get(root); final NodeConverterInfo<InputType, OutputType> converterInfo = this.getNodeConverterInfo(root.getClass()); for (final GraphConversionListener<InputType, OutputType> listener : this.conversionListener) listener.beforeSubgraphConversion(root); final List<OutputType> childTypes = this.convertChildren(navigator, root, converterInfo); this.lastChildren = converterInfo != null && converterInfo.shouldAppendChildren() ? childTypes.subList( converterInfo.getAppendIndex(), childTypes.size()) : Collections.EMPTY_LIST; final OutputType convertedType = this.convertNode(root, childTypes); for (final GraphConversionListener<InputType, OutputType> listener : this.conversionListener) listener.afterSubgraphConversion(root, convertedType); if (convertedType == null) return childTypes.isEmpty() ? null : childTypes.get(0); return convertedType; } @Override public OutputType convertNode(final InputType in, final List<OutputType> children) { final NodeConverterInfo<InputType, OutputType> converterInfo = this.getNodeConverterInfo(in.getClass()); if (converterInfo == null) return null; try { for (final GraphConversionListener<InputType, OutputType> listener : this.conversionListener) listener.beforeNodeConversion(in, children); final OutputType converted = converterInfo.getConverter().convertNode(in, children); for (final GraphConversionListener<InputType, OutputType> listener : this.conversionListener) listener.afterNodeConversion(in, children, converted); return converted; } catch (final Exception e) { throw new IllegalStateException(e); } } /** * Returns the registered converter for the given node class or base classes. * * @param clazz * the node class * @return the registered converter for the given node class */ public NodeConverter<InputType, OutputType> getNodeConverter(final Class<? extends InputType> clazz) { final NodeConverterInfo<InputType, OutputType> converterInfo = this.getNodeConverterInfo(clazz); return converterInfo == null ? null : converterInfo.converter; } private NodeConverterInfo<InputType, OutputType> getNodeConverterInfo(final Class<?> clazz) { NodeConverterInfo<InputType, OutputType> converterInfo = this.converterInfos.get(clazz); if (converterInfo == null && clazz.getSuperclass() != null) { converterInfo = this.getNodeConverterInfo(clazz.getSuperclass()); if (converterInfo != null) this.converterInfos.put(clazz, converterInfo); } return converterInfo; } /** * Registers the {@link NodeConverter} for the specified types. * * @param <BaseInputType> * the base type for all types * @param converter * the converter to register * @param types * a list of all types for which * @return this */ public <BaseInputType extends InputType> GraphConverter<InputType, OutputType> register( final NodeConverter<? extends BaseInputType, ? extends OutputType> converter, final List<Class<? extends BaseInputType>> types) { final NodeConverterInfo<InputType, OutputType> converterInfo = new NodeConverterInfo<InputType, OutputType>( converter); for (final Class<? extends InputType> type : types) this.converterInfos.put(type, converterInfo); return this; } /** * Registers the {@link NodeConverter} for the given BaseInputType inferred from the static bound of the generic * class parameter in the NodeConverter. * * @param converter * the converter to register * @param <BaseInputType> * the base type * @return this */ @SuppressWarnings("unchecked") public <BaseInputType extends InputType> GraphConverter<InputType, OutputType> register( final NodeConverter<BaseInputType, ? extends OutputType> converter) { final OneElementList<Class<? extends BaseInputType>> wrapList = new OneElementList<Class<? extends BaseInputType>>( (Class<? extends BaseInputType>) BoundTypeUtil .getBindingOfSuperclass(converter.getClass(), NodeConverter.class).getParameters()[0].getType()); this.register(converter, wrapList); return this; } /** * Registers the {@link NodeConverter}s for the types inferred from the static bound of the generic * class parameter in the NodeConverter. * * @param converters * the converters to register * @return this */ public GraphConverter<InputType, OutputType> registerAll( final NodeConverter<? extends InputType, ? extends OutputType>... converters) { for (final NodeConverter<? extends InputType, ? extends OutputType> converter : converters) this.register(converter); return this; } /** * Removes a {@link GraphConversionListener} from this GraphConverter. * * @param listener * the listener to remove */ public void removeListener(final GraphConversionListener<InputType, OutputType> listener) { this.conversionListener.remove(listener); } /** * Removes the {@link NodeConverter} for the given type. * * @param type * the type of which the associated NodeConverter is removed */ public void unregister(final Class<? extends InputType> type) { this.converterInfos.remove(type); } /** * Removes the given {@link NodeConverter}. * * @param converter * the converter to remove */ public void unregister(final NodeConverter<InputType, OutputType> converter) { final Iterator<? extends Entry<?, ?>> iterator = this.converterInfos.entrySet().iterator(); for (; iterator.hasNext();) if (iterator.next().getValue() == converter) iterator.remove(); } /** * Holds additional information about a converter mostly extracted from annotations. * * @author Arvid Heise */ private static class NodeConverterInfo<InputType, OutputBase> { private int appendIndex = -1; private boolean stopRecursion = false; private final NodeConverter<InputType, OutputBase> converter; @SuppressWarnings("unchecked") public NodeConverterInfo(final NodeConverter<? extends InputType, ? extends OutputBase> converter) { this.converter = (NodeConverter<InputType, OutputBase>) converter; final Class<?> converterClass = converter.getClass(); final AppendChildren annotation = converterClass.getAnnotation(AppendChildren.class); if (annotation != null) this.appendIndex = annotation.fromIndex(); this.stopRecursion = converterClass.getAnnotation(Leaf.class) != null; } public int getAppendIndex() { return this.appendIndex; } public NodeConverter<InputType, OutputBase> getConverter() { return this.converter; } public boolean isStopRecursion() { return this.stopRecursion; } public boolean shouldAppendChildren() { return this.appendIndex != -1; } } }