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