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}