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.nuxeo.ecm.automation.AutomationService;
022import org.nuxeo.ecm.automation.CompiledChain;
023import org.nuxeo.ecm.automation.ExitException;
024import org.nuxeo.ecm.automation.InvalidChainException;
025import org.nuxeo.ecm.automation.OperationContext;
026import org.nuxeo.ecm.automation.OperationException;
027import org.nuxeo.ecm.automation.OperationNotFoundException;
028import org.nuxeo.ecm.automation.OperationParameters;
029import org.nuxeo.ecm.automation.OperationType;
030import org.nuxeo.ecm.automation.core.scripting.Expression;
031
032import com.google.common.cache.CacheBuilder;
033import com.google.common.cache.CacheLoader;
034import com.google.common.cache.LoadingCache;
035
036public class OperationChainCompiler {
037
038    protected final AutomationService service;
039
040    protected final LoadingCache<Connector, OperationMethod> cache;
041
042    protected static final int MAX_CACHE_SIZE = 1000;
043
044    protected OperationChainCompiler(AutomationService service) {
045        this.service = service;
046        cache = CacheBuilder.newBuilder() //
047                            .maximumSize(MAX_CACHE_SIZE)
048                            .build(new CacheLoader<Connector, OperationMethod>() {
049                                @Override
050                                public OperationMethod load(Connector connector) throws OperationException {
051                                    return connector.connect();
052                                }
053                            });
054    }
055
056    public CompiledChain compile(ChainTypeImpl typeof, Class<?> typein) throws OperationException {
057        Connector connector = new Connector(typeof, typein);
058        OperationMethod operationMethod;
059        try {
060            operationMethod = cache.get(connector);
061        } catch (ExecutionException e) {
062            Throwable cause = e.getCause();
063            if (cause instanceof OperationException) {
064                throw (OperationException) cause;
065            } else {
066                throw new OperationException(cause);
067            }
068        }
069        return new CompiledChainImpl(typeof, typein, operationMethod);
070    }
071
072    protected class Connector {
073
074        protected final ChainTypeImpl typeof;
075
076        protected final Class<?> typein;
077
078        protected Connector(ChainTypeImpl typeof, Class<?> typein) {
079            this.typeof = typeof;
080            this.typein = typein;
081        }
082
083        @Override
084        public int hashCode() {
085            int prime = 31;
086            int result = 1;
087            result = prime * result + typeof.hashCode();
088            result = prime * result + typein.hashCode();
089            return result;
090        }
091
092        @Override
093        public boolean equals(Object obj) {
094            if (this == obj) {
095                return true;
096            }
097            if (obj == null) {
098                return false;
099            }
100            if (!(obj instanceof Connector)) {
101                return false;
102            }
103            Connector other = (Connector) obj;
104            return typeof.equals(other.typeof) && typein.equals(other.typein);
105        }
106
107        protected OperationMethod connect() throws OperationException {
108            OperationMethod head = null;
109            OperationMethod prev = null;
110            for (OperationParameters params : typeof.chain.getOperations()) {
111                OperationMethod next = new OperationMethod(params, prev);
112                if (prev != null) {
113                    prev.next = next;
114                }
115                if (next.prev == null) {
116                    head = next;
117                }
118                prev = next;
119            }
120            if (head != null) {
121                head.solve(typein);
122            }
123            return head;
124        }
125    }
126
127    protected class OperationMethod {
128
129        protected final OperationType typeof;
130
131        protected final OperationParameters params;
132
133        protected InvokableMethod method;
134
135        protected OperationMethod prev;
136
137        protected OperationMethod next;
138
139        protected OperationMethod(OperationParameters params, OperationMethod prev) throws OperationNotFoundException {
140            typeof = service.getOperation(params.id());
141            this.params = params;
142            this.prev = prev;
143        }
144
145        protected Object invoke(OperationContext context) throws OperationException {
146            context.getCallback().onOperationEnter(context, typeof, method, params.map());
147            Object output = method.invoke(context, params.map());
148            if (output instanceof Expression) {
149                output = ((Expression) output).eval(context);
150            }
151            context.getCallback().onOperationExit(output);
152            context.setInput(output);
153            if (next != null) {
154                return next.invoke(context);
155            }
156            return output;
157        }
158
159        /**
160         * Compute the best matching path to perform the chain of operations. The path is computed using a backtracking
161         * algorithm.
162         */
163        void solve(Class<?> in) throws InvalidChainException {
164            InvokableMethod[] methods = typeof.getMethodsMatchingInput(in);
165            if (methods.length == 0) {
166                throw new InvalidChainException(
167                        "Cannot find any valid path in operation chain - no method found for operation '"
168                                + typeof.getId() + "' and for first input type '" + in.getName() + "'");
169            }
170            if (next == null) {
171                method = methods[0];
172                return;
173            }
174            for (InvokableMethod m : methods) {
175                Class<?> nextIn = m.getOutputType();
176                if (nextIn == Void.TYPE || nextIn.equals(Object.class)) {
177                    nextIn = in; // preserve last input
178                }
179                try {
180                    next.solve(nextIn);
181                    method = m;
182                    return;
183                } catch (InvalidChainException cause) {
184                    // continue solving
185                }
186            }
187            throw new InvalidChainException(
188                    "Cannot find any valid path in operation chain - no method found for operation '" + typeof.getId()
189                            + "' and for first input type '" + in.getName() + "'");
190        }
191    }
192
193    protected class CompiledChainImpl implements CompiledChain {
194
195        protected final ChainTypeImpl typeof;
196
197        protected final Class<?> typein;
198
199        protected final OperationMethod head;
200
201        protected CompiledChainImpl(ChainTypeImpl typeof, Class<?> typein, OperationMethod head) {
202            this.typeof = typeof;
203            this.typein = typein;
204            this.head = head;
205        }
206
207        @Override
208        public Object invoke(OperationContext ctx) throws OperationException {
209            return ctx.callWithChainParameters(() -> {
210                ctx.getCallback().onChainEnter(typeof);
211                try {
212                    return head.invoke(ctx);
213                } catch (ExitException e) {
214                    if (e.isRollback()) {
215                        ctx.setRollback();
216                    }
217                    return ctx.getInput();
218                } catch (OperationException op) {
219                    throw ctx.getCallback().onError(op);
220                } finally {
221                    ctx.getCallback().onChainExit();
222                }
223            }, typeof.getChainParameters());
224        }
225
226        @Override
227        public String toString() {
228            return "CompiledChainImpl [op=" + typeof + "," + "input=" + typein + "]";
229        }
230
231    }
232
233}