001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     bstefanescu
011 */
012package org.nuxeo.ecm.automation.core.impl;
013
014import java.lang.reflect.Method;
015import java.lang.reflect.Type;
016import java.util.Iterator;
017import java.util.Map;
018
019import org.nuxeo.ecm.automation.OperationContext;
020import org.nuxeo.ecm.automation.OperationException;
021import org.nuxeo.ecm.automation.OperationType;
022import org.nuxeo.ecm.automation.OutputCollector;
023import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
024
025/**
026 * A method proxy which accept as input only iterable inputs. At invocation time it iterates over the input elements and
027 * invoke the real method using the current input element as the input.
028 * <p>
029 * The result is collected into a {@link OutputCollector} as specified by the {@link OperationMethod} annotation.
030 * <p>
031 * This proxy is used (instead of the default {@link InvokableMethod}) when an operation is defining an output collector
032 * in the {@link OperationMethod} annotation so that iterable inputs are automatically handled by the chain executor.
033 * <p>
034 * This specialized implementation is declaring the same consume type as its non-iterable counterpart. But at runtime it
035 * consumes any Iterable of the cosume type.
036 * <p>
037 * To correctly generate the operation documentation the {@link OperationTypeImpl} is checking if the method is iterable
038 * or not through {@link #isIterable()} to declare the correct consume type.
039 *
040 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
041 */
042public class InvokableIteratorMethod extends InvokableMethod {
043
044    @SuppressWarnings("rawtypes")
045    protected Class<? extends OutputCollector> collector;
046
047    public InvokableIteratorMethod(OperationType op, Method method, OperationMethod anno) {
048        super(op, method, anno);
049        collector = anno.collector();
050        if (collector == OutputCollector.class) {
051            throw new IllegalArgumentException("Not an iterable method");
052        }
053        // check the collector match the method signature - to early detect invalid
054        // operation definitions.
055        if (consume == Void.TYPE) {
056            throw new IllegalArgumentException("An iterable method must have an argument");
057        }
058        Type[] ctypes = IterableInputHelper.findCollectorTypes(collector);
059        if (!((Class<?>) ctypes[0]).isAssignableFrom(produce)) {
060            throw new IllegalArgumentException("The collector used on " + method
061                    + " doesn't match the method return type");
062        }
063        // must modify the produced type to fit the real produced type.
064        try {
065            produce = (Class<?>) ctypes[1];
066        } catch (ArrayIndexOutOfBoundsException e) {
067            throw new IllegalStateException("Invalid output collector: " + collector + ". No getOutput method found.");
068        }
069        // the consumed type is not used in chain compilation so we let it as is
070        // for now.
071    }
072
073    @Override
074    public boolean isIterable() {
075        return true;
076    }
077
078    @Override
079    public int inputMatch(Class<?> in) {
080        Class<?> iterableIn = IterableInputHelper.getIterableType(in);
081        if (iterableIn != null) {
082            return super.inputMatch(iterableIn);
083        }
084        return 0;
085    }
086
087    @SuppressWarnings({ "rawtypes", "unchecked" })
088    @Override
089    protected Object doInvoke(OperationContext ctx, Map<String, Object> args, Object input) throws OperationException,
090            ReflectiveOperationException {
091        if (!(input instanceof Iterable)) {
092            throw new IllegalStateException("An iterable method was called in a non iterable context");
093        }
094        OutputCollector list = collector.newInstance();
095        Iterable<?> iterable = (Iterable<?>) input;
096        Iterator<?> it = iterable.iterator();
097        while (it.hasNext()) {
098            Object in = it.next();
099            // update context to use as input the current entry
100            ctx.setInput(in);
101            list.collect(ctx, super.doInvoke(ctx, args, in));
102        }
103        return list.getOutput();
104    }
105
106}