001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 */
017package org.nuxeo.ecm.automation.core.impl;
018
019import java.util.concurrent.ExecutionException;
020
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023import org.nuxeo.ecm.automation.AutomationService;
024import org.nuxeo.ecm.automation.CompiledChain;
025import org.nuxeo.ecm.automation.ExitException;
026import org.nuxeo.ecm.automation.InvalidChainException;
027import org.nuxeo.ecm.automation.OperationContext;
028import org.nuxeo.ecm.automation.OperationException;
029import org.nuxeo.ecm.automation.OperationNotFoundException;
030import org.nuxeo.ecm.automation.OperationParameters;
031import org.nuxeo.ecm.automation.OperationType;
032import org.nuxeo.ecm.automation.core.scripting.Expression;
033
034import com.google.common.cache.CacheBuilder;
035import com.google.common.cache.CacheLoader;
036import com.google.common.cache.LoadingCache;
037
038public class OperationChainCompiler {
039
040    private static final Log log = LogFactory.getLog(OperationChainCompiler.class);
041
042    protected final AutomationService service;
043
044    protected final LoadingCache<Connector, OperationMethod> cache;
045
046    protected static final int MAX_CACHE_SIZE = 1000;
047
048    protected OperationChainCompiler(AutomationService service) {
049        this.service = service;
050        cache = CacheBuilder.newBuilder() //
051                            .maximumSize(MAX_CACHE_SIZE)
052                            .build(new CacheLoader<Connector, OperationMethod>() {
053                                @Override
054                                public OperationMethod load(Connector connector) throws OperationException {
055                                    return connector.connect();
056                                }
057                            });
058    }
059
060    public CompiledChain compile(ChainTypeImpl typeof, Class<?> typein) throws OperationException {
061        Connector connector = new Connector(typeof, typein);
062        OperationMethod operationMethod;
063        try {
064            operationMethod = cache.get(connector);
065        } catch (ExecutionException e) {
066            Throwable cause = e.getCause();
067            if (cause instanceof OperationException) {
068                throw (OperationException) cause;
069            } else {
070                throw new OperationException(cause);
071            }
072        }
073        return new CompiledChainImpl(typeof, typein, operationMethod);
074    }
075
076    protected class Connector {
077
078        protected final ChainTypeImpl typeof;
079
080        protected final Class<?> typein;
081
082        protected Connector(ChainTypeImpl typeof, Class<?> typein) {
083            this.typeof = typeof;
084            this.typein = typein;
085        }
086
087        @Override
088        public int hashCode() {
089            int prime = 31;
090            int result = 1;
091            result = prime * result + typeof.hashCode();
092            result = prime * result + typein.hashCode();
093            return result;
094        }
095
096        @Override
097        public boolean equals(Object obj) {
098            if (this == obj) {
099                return true;
100            }
101            if (obj == null) {
102                return false;
103            }
104            if (!(obj instanceof Connector)) {
105                return false;
106            }
107            Connector other = (Connector) obj;
108            return typeof.equals(other.typeof) && typein.equals(other.typein);
109        }
110
111        protected OperationMethod connect() throws OperationException {
112            OperationMethod head = null;
113            OperationMethod prev = null;
114            for (OperationParameters params : typeof.chain.getOperations()) {
115                OperationMethod next = new OperationMethod(params, prev);
116                if (prev != null) {
117                    prev.next = next;
118                }
119                if (next.prev == null) {
120                    head = next;
121                }
122                prev = next;
123            }
124            if (head != null) {
125                head.solve(typein);
126            }
127            return head;
128        }
129    }
130
131    protected class OperationMethod {
132
133        protected final OperationType typeof;
134
135        protected final OperationParameters params;
136
137        protected InvokableMethod method;
138
139        protected OperationMethod prev;
140
141        protected OperationMethod next;
142
143        protected OperationMethod(OperationParameters params, OperationMethod prev) throws OperationNotFoundException {
144            typeof = service.getOperation(params.id());
145            this.params = params;
146            this.prev = prev;
147        }
148
149        protected Object invoke(OperationContext context) throws OperationException {
150            context.getCallback().onOperationEnter(context, typeof, method, params.map());
151            Object output = method.invoke(context, params.map());
152            if (output instanceof Expression) {
153                output = ((Expression) output).eval(context);
154            }
155            context.getCallback().onOperationExit(output);
156            context.setInput(output);
157            if (next != null) {
158                return next.invoke(context);
159            }
160            return output;
161        }
162
163        /**
164         * Compute the best matching path to perform the chain of operations. The path is computed using a backtracking
165         * algorithm.
166         *
167         * @throws InvalidChainException
168         */
169        void solve(Class<?> in) throws InvalidChainException {
170            InvokableMethod[] methods = typeof.getMethodsMatchingInput(in);
171            if (methods.length == 0) {
172                throw new InvalidChainException(
173                        "Cannot find any valid path in operation chain - no method found for operation '"
174                                + typeof.getId() + "' and for first input type '" + in.getName() + "'");
175            }
176            if (next == null) {
177                method = methods[0];
178                return;
179            }
180            for (InvokableMethod m : methods) {
181                Class<?> nextIn = m.getOutputType();
182                if (nextIn == Void.TYPE || nextIn.equals(Object.class)) {
183                    nextIn = in; // preserve last input
184                }
185                try {
186                    next.solve(nextIn);
187                    method = m;
188                    return;
189                } catch (InvalidChainException cause) {
190                    // continue solving
191                }
192            }
193            throw new InvalidChainException(
194                    "Cannot find any valid path in operation chain - no method found for operation '" + typeof.getId()
195                            + "' and for first input type '" + in.getName() + "'");
196        }
197    }
198
199    protected class CompiledChainImpl implements CompiledChain {
200
201        protected final ChainTypeImpl typeof;
202
203        protected final Class<?> typein;
204
205        protected final OperationMethod head;
206
207        protected CompiledChainImpl(ChainTypeImpl typeof, Class<?> typein, OperationMethod head) {
208            this.typeof = typeof;
209            this.typein = typein;
210            this.head = head;
211        }
212
213        @Override
214        public Object invoke(OperationContext ctx) throws OperationException {
215            return ctx.callWithChainParameters(() -> {
216                ctx.getCallback().onChainEnter(typeof);
217                try {
218                    return head.invoke(ctx);
219                } catch (ExitException e) {
220                    if (e.isRollback()) {
221                        ctx.setRollback();
222                    }
223                    return ctx.getInput();
224                } catch (OperationException op) {
225                    throw ctx.getCallback().onError(op);
226                } finally {
227                    ctx.getCallback().onChainExit();
228                }
229            }, typeof.getChainParameters());
230        }
231
232        @Override
233        public String toString() {
234            return "CompiledChainImpl [op=" + typeof + "," + "input=" + typein + "]";
235        }
236
237    }
238
239}