2394b83d3d24ce13252c5050e0729cdb7b54f7a2
[policy/apex-pdp.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  * Copyright (C) 2020 Nordix Foundation.
4  * ================================================================================
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.apex.plugins.executor.javascript;
22
23 import java.util.concurrent.BlockingQueue;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.LinkedBlockingQueue;
26 import java.util.concurrent.atomic.AtomicReference;
27
28 import org.apache.commons.lang3.StringUtils;
29 import org.mozilla.javascript.Context;
30 import org.mozilla.javascript.Script;
31 import org.mozilla.javascript.Scriptable;
32 import org.onap.policy.apex.core.engine.executor.exception.StateMachineException;
33 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
34 import org.slf4j.ext.XLogger;
35 import org.slf4j.ext.XLoggerFactory;
36
37 /**
38  * The Class JavascriptExecutor is the executor for task logic written in Javascript.
39  *
40  * @author Liam Fallon (liam.fallon@ericsson.com)
41  */
42 public class JavascriptExecutor implements Runnable {
43     private static final XLogger LOGGER = XLoggerFactory.getXLogger(JavascriptExecutor.class);
44
45     public static final int DEFAULT_OPTIMIZATION_LEVEL = 9;
46
47     // Recurring string constants
48     private static final String WITH_MESSAGE = " with message: ";
49
50     // The key of the subject that wants to execute Javascript code
51     final AxKey subjectKey;
52
53     private String javascriptCode;
54     private Context javascriptContext;
55     private Script script;
56
57     private final BlockingQueue<Object> executionQueue = new LinkedBlockingQueue<>();
58     private final BlockingQueue<Boolean> resultQueue = new LinkedBlockingQueue<>();
59
60     private final Thread executorThread;
61     private CountDownLatch intializationLatch = new CountDownLatch(1);
62     private CountDownLatch shutdownLatch = new CountDownLatch(1);
63     private AtomicReference<StateMachineException> executorException = new AtomicReference<>(null);
64
65     /**
66      * Initializes the Javascript executor.
67      *
68      * @param subjectKey the key of the subject that is requesting Javascript execution
69      */
70     public JavascriptExecutor(final AxKey subjectKey) {
71         this.subjectKey = subjectKey;
72
73         executorThread = new Thread(this);
74         executorThread.setName(this.getClass().getSimpleName() + ":" + subjectKey.getId());
75     }
76
77     /**
78      * Prepares the executor for processing and compiles the Javascript code.
79      *
80      * @param javascriptCode the Javascript code to execute
81      * @throws StateMachineException thrown when instantiation of the executor fails
82      */
83     public void init(final String javascriptCode) throws StateMachineException {
84         LOGGER.debug("JavascriptExecutor {} starting ... ", subjectKey.getId());
85
86         if (executorThread.isAlive()) {
87             throw new StateMachineException(
88                 "initiation failed, executor " + subjectKey.getId() + " is already running");
89         }
90
91         if (StringUtils.isEmpty(javascriptCode)) {
92             throw new StateMachineException("no logic specified for " + subjectKey.getId());
93         }
94
95         this.javascriptCode = javascriptCode;
96
97         try {
98             executorThread.start();
99         } catch (Exception e) {
100             throw new StateMachineException("initiation failed, executor " + subjectKey.getId() + " failed to start",
101                 e);
102         }
103
104         try {
105             intializationLatch.await();
106         } catch (InterruptedException e) {
107             LOGGER.debug("JavascriptExecutor {} interrupted on execution thread startup", subjectKey.getId(), e);
108             Thread.currentThread().interrupt();
109         }
110
111         if (executorException.get() != null) {
112             clearAndThrowExecutorException();
113         }
114
115         LOGGER.debug("JavascriptExecutor {} started ... ", subjectKey.getId());
116     }
117
118     /**
119      * Execute a Javascript script.
120      *
121      * @param executionContext the execution context to use for script execution
122      * @return true if execution was successful, false otherwise
123      * @throws StateMachineException on execution errors
124      */
125     public boolean execute(final Object executionContext) throws StateMachineException {
126         if (!executorThread.isAlive()) {
127             throw new StateMachineException("execution failed, executor " + subjectKey.getId() + " is not running");
128         }
129
130         executionQueue.add(executionContext);
131
132         boolean result = false;
133
134         try {
135             result = resultQueue.take();
136         } catch (final InterruptedException e) {
137             LOGGER.debug("JavascriptExecutor {} interrupted on execution result wait", subjectKey.getId(), e);
138             Thread.currentThread().interrupt();
139         }
140
141         if (executorException.get() != null) {
142             clearAndThrowExecutorException();
143         }
144
145         return result;
146     }
147
148     /**
149      * Cleans up the executor after processing.
150      *
151      * @throws StateMachineException thrown when cleanup of the executor fails
152      */
153     public void cleanUp() throws StateMachineException {
154         if (!executorThread.isAlive()) {
155             throw new StateMachineException("cleanup failed, executor " + subjectKey.getId() + " is not running");
156         }
157
158         executorThread.interrupt();
159
160         try {
161             shutdownLatch.await();
162         } catch (InterruptedException e) {
163             LOGGER.debug("JavascriptExecutor {} interrupted on execution clkeanup wait", subjectKey.getId(), e);
164             Thread.currentThread().interrupt();
165         }
166
167         if (executorException.get() != null) {
168             clearAndThrowExecutorException();
169         }
170     }
171
172     @Override
173     public void run() {
174         LOGGER.debug("JavascriptExecutor {} initializing ... ", subjectKey.getId());
175
176         try {
177             initExecutor();
178         } catch (StateMachineException sme) {
179             LOGGER.warn("JavascriptExecutor {} initialization failed", sme);
180             executorException.set(sme);
181             intializationLatch.countDown();
182             return;
183         }
184
185         intializationLatch.countDown();
186
187         LOGGER.debug("JavascriptExecutor {} executing ... ", subjectKey.getId());
188
189         // Take jobs from the execution queue of the worker and execute them
190         while (!executorThread.isInterrupted()) {
191             try {
192                 Object contextObject = executionQueue.take();
193                 if (contextObject == null) {
194                     break;
195                 }
196
197                 resultQueue.add(executeScript(contextObject));
198             } catch (final InterruptedException e) {
199                 LOGGER.debug("execution was interruped for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
200                 resultQueue.add(false);
201                 Thread.currentThread().interrupt();
202             } catch (StateMachineException sme) {
203                 executorException.set(sme);
204                 resultQueue.add(false);
205             }
206         }
207
208         try {
209             Context.exit();
210         } catch (final Exception e) {
211             executorException.set(new StateMachineException(
212                 "executor close failed to close for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e));
213         }
214
215         shutdownLatch.countDown();
216
217         LOGGER.debug("JavascriptExecutor {} completed processing", subjectKey.getId());
218     }
219
220     private void initExecutor() throws StateMachineException {
221         try {
222             // Create a Javascript context for this thread
223             javascriptContext = Context.enter();
224
225             // Set up the default values of the context
226             javascriptContext.setOptimizationLevel(DEFAULT_OPTIMIZATION_LEVEL);
227             javascriptContext.setLanguageVersion(Context.VERSION_1_8);
228
229             script = javascriptContext.compileString(javascriptCode, subjectKey.getId(), 1, null);
230         } catch (Exception e) {
231             Context.exit();
232             throw new StateMachineException(
233                 "logic failed to compile for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
234         }
235     }
236
237     private boolean executeScript(final Object executionContext) throws StateMachineException {
238         Object returnObject = null;
239
240         try {
241             // Pass the subject context to the Javascript engine
242             Scriptable javascriptScope = javascriptContext.initStandardObjects();
243             javascriptScope.put("executor", javascriptScope, executionContext);
244
245             // Run the script
246             returnObject = script.exec(javascriptContext, javascriptScope);
247         } catch (final Exception e) {
248             throw new StateMachineException(
249                 "logic failed to run for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
250         }
251
252         if (!(returnObject instanceof Boolean)) {
253             throw new StateMachineException(
254                 "execute: logic for " + subjectKey.getId() + " returned a non-boolean value " + returnObject);
255         }
256
257         return (boolean) returnObject;
258     }
259
260     private void clearAndThrowExecutorException() throws StateMachineException {
261         StateMachineException exceptionToThrow = executorException.getAndSet(null);
262         if (exceptionToThrow != null) {
263             throw exceptionToThrow;
264         }
265     }
266
267     protected Thread getExecutorThread() {
268         return executorThread;
269     }
270 }