Changes for checkstyle 8.32
[policy/apex-pdp.git] / plugins / plugins-executor / plugins-executor-javascript / src / main / java / org / onap / policy / apex / plugins / executor / javascript / JavascriptExecutor.java
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.TimeUnit;
27 import java.util.concurrent.atomic.AtomicReference;
28 import lombok.AccessLevel;
29 import lombok.Getter;
30 import lombok.NonNull;
31 import lombok.Setter;
32 import org.apache.commons.lang3.StringUtils;
33 import org.mozilla.javascript.Context;
34 import org.mozilla.javascript.Script;
35 import org.mozilla.javascript.Scriptable;
36 import org.onap.policy.apex.core.engine.executor.exception.StateMachineException;
37 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
38 import org.slf4j.ext.XLogger;
39 import org.slf4j.ext.XLoggerFactory;
40
41 /**
42  * The Class JavascriptExecutor is the executor for task logic written in Javascript.
43  *
44  * @author Liam Fallon (liam.fallon@ericsson.com)
45  */
46 public class JavascriptExecutor implements Runnable {
47     private static final XLogger LOGGER = XLoggerFactory.getXLogger(JavascriptExecutor.class);
48
49     public static final int DEFAULT_OPTIMIZATION_LEVEL = 9;
50
51     // Token passed to executor thread to stop execution
52     private static final Object STOP_EXECUTION_TOKEN = "*** STOP EXECUTION ***";
53
54     // Recurring string constants
55     private static final String WITH_MESSAGE = " with message: ";
56     private static final String JAVASCRIPT_EXECUTOR = "JavascriptExecutor ";
57     private static final String EXECUTION_FAILED_EXECUTOR = "execution failed, executor ";
58
59     @Setter(AccessLevel.PROTECTED)
60     private static TimeUnit timeunit4Latches = TimeUnit.SECONDS;
61     @Setter(AccessLevel.PROTECTED)
62     private static int intializationLatchTimeout = 60;
63     @Setter(AccessLevel.PROTECTED)
64     private static int cleanupLatchTimeout = 60;
65
66     // The key of the subject that wants to execute Javascript code
67     final AxKey subjectKey;
68
69     private String javascriptCode;
70     private Context javascriptContext;
71     private Script script;
72
73     private final BlockingQueue<Object> executionQueue = new LinkedBlockingQueue<>();
74     private final BlockingQueue<Boolean> resultQueue = new LinkedBlockingQueue<>();
75
76     @Getter(AccessLevel.PROTECTED)
77     private Thread executorThread;
78     private CountDownLatch intializationLatch;
79     private CountDownLatch cleanupLatch;
80     private AtomicReference<StateMachineException> executorException = new AtomicReference<>(null);
81
82     /**
83      * Initializes the Javascript executor.
84      *
85      * @param subjectKey the key of the subject that is requesting Javascript execution
86      */
87     public JavascriptExecutor(final AxKey subjectKey) {
88         this.subjectKey = subjectKey;
89     }
90
91     /**
92      * Prepares the executor for processing and compiles the Javascript code.
93      *
94      * @param javascriptCode the Javascript code to execute
95      * @throws StateMachineException thrown when instantiation of the executor fails
96      */
97     public synchronized void init(@NonNull final String javascriptCode) throws StateMachineException {
98         LOGGER.debug("JavascriptExecutor {} starting ... ", subjectKey.getId());
99
100         if (executorThread != null) {
101             throw new StateMachineException("initiation failed, executor " + subjectKey.getId()
102                 + " already initialized, run cleanUp to clear executor");
103         }
104
105         if (StringUtils.isBlank(javascriptCode)) {
106             throw new StateMachineException("initiation failed, no logic specified for executor " + subjectKey.getId());
107         }
108
109         this.javascriptCode = javascriptCode;
110
111         executorThread = new Thread(this);
112         executorThread.setName(this.getClass().getSimpleName() + ":" + subjectKey.getId());
113         intializationLatch = new CountDownLatch(1);
114         cleanupLatch = new CountDownLatch(1);
115
116         try {
117             executorThread.start();
118         } catch (IllegalThreadStateException e) {
119             throw new StateMachineException("initiation failed, executor " + subjectKey.getId() + " failed to start",
120                 e);
121         }
122
123         try {
124             if (!intializationLatch.await(intializationLatchTimeout, timeunit4Latches)) {
125                 executorThread.interrupt();
126                 throw new StateMachineException(JAVASCRIPT_EXECUTOR + subjectKey.getId()
127                     + " initiation timed out after " + intializationLatchTimeout + " " + timeunit4Latches);
128             }
129         } catch (InterruptedException e) {
130             LOGGER.debug("JavascriptExecutor {} interrupted on execution thread startup", subjectKey.getId(), e);
131             Thread.currentThread().interrupt();
132         }
133
134         if (executorException.get() != null) {
135             executorThread.interrupt();
136             checkAndThrowExecutorException();
137         }
138
139         checkAndThrowExecutorException();
140
141         LOGGER.debug("JavascriptExecutor {} started ... ", subjectKey.getId());
142     }
143
144     /**
145      * Execute a Javascript script.
146      *
147      * @param executionContext the execution context to use for script execution
148      * @return true if execution was successful, false otherwise
149      * @throws StateMachineException on execution errors
150      */
151     public synchronized boolean execute(final Object executionContext) throws StateMachineException {
152         if (executorThread == null) {
153             throw new StateMachineException(EXECUTION_FAILED_EXECUTOR + subjectKey.getId() + " is not initialized");
154         }
155
156         if (!executorThread.isAlive() || executorThread.isInterrupted()) {
157             throw new StateMachineException(EXECUTION_FAILED_EXECUTOR + subjectKey.getId()
158                 + " is not running, run cleanUp to clear executor and init to restart executor");
159         }
160
161         executionQueue.add(executionContext);
162
163         boolean result = false;
164
165         try {
166             result = resultQueue.take();
167         } catch (final InterruptedException e) {
168             executorThread.interrupt();
169             Thread.currentThread().interrupt();
170             throw new StateMachineException(
171                 JAVASCRIPT_EXECUTOR + subjectKey.getId() + "interrupted on execution result wait", e);
172         }
173
174         checkAndThrowExecutorException();
175
176         return result;
177     }
178
179     /**
180      * Cleans up the executor after processing.
181      *
182      * @throws StateMachineException thrown when cleanup of the executor fails
183      */
184     public synchronized void cleanUp() throws StateMachineException {
185         if (executorThread == null) {
186             throw new StateMachineException("cleanup failed, executor " + subjectKey.getId() + " is not initialized");
187         }
188
189         if (executorThread.isAlive()) {
190             executionQueue.add(STOP_EXECUTION_TOKEN);
191
192             try {
193                 if (!cleanupLatch.await(cleanupLatchTimeout, timeunit4Latches)) {
194                     executorException.set(new StateMachineException(JAVASCRIPT_EXECUTOR + subjectKey.getId()
195                         + " cleanup timed out after " + cleanupLatchTimeout + " " + timeunit4Latches));
196                 }
197             } catch (InterruptedException e) {
198                 LOGGER.debug("JavascriptExecutor {} interrupted on execution cleanup wait", subjectKey.getId(), e);
199                 Thread.currentThread().interrupt();
200             }
201         }
202
203         executorThread = null;
204         executionQueue.clear();
205         resultQueue.clear();
206
207         checkAndThrowExecutorException();
208     }
209
210     @Override
211     public void run() {
212         LOGGER.debug("JavascriptExecutor {} initializing ... ", subjectKey.getId());
213
214         try {
215             initExecutor();
216         } catch (StateMachineException sme) {
217             LOGGER.warn("JavascriptExecutor {} initialization failed", subjectKey.getId(), sme);
218             executorException.set(sme);
219             intializationLatch.countDown();
220             cleanupLatch.countDown();
221             return;
222         }
223
224         intializationLatch.countDown();
225
226         LOGGER.debug("JavascriptExecutor {} executing ... ", subjectKey.getId());
227
228         // Take jobs from the execution queue of the worker and execute them
229         while (!Thread.currentThread().isInterrupted()) {
230             try {
231                 Object contextObject = executionQueue.take();
232                 if (STOP_EXECUTION_TOKEN.equals(contextObject)) {
233                     LOGGER.debug("execution close was ordered for  " + subjectKey.getId());
234                     break;
235                 }
236                 resultQueue.add(executeScript(contextObject));
237             } catch (final InterruptedException e) {
238                 LOGGER.debug("execution was interruped for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
239                 executionQueue.add(STOP_EXECUTION_TOKEN);
240                 Thread.currentThread().interrupt();
241             } catch (StateMachineException sme) {
242                 executorException.set(sme);
243                 resultQueue.add(false);
244             }
245         }
246
247         resultQueue.add(false);
248
249         try {
250             Context.exit();
251         } catch (final Exception e) {
252             executorException.set(new StateMachineException(
253                 "executor close failed to close for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e));
254         }
255
256         cleanupLatch.countDown();
257
258         LOGGER.debug("JavascriptExecutor {} completed processing", subjectKey.getId());
259     }
260
261     private void initExecutor() throws StateMachineException {
262         try {
263             // Create a Javascript context for this thread
264             javascriptContext = Context.enter();
265
266             // Set up the default values of the context
267             javascriptContext.setOptimizationLevel(DEFAULT_OPTIMIZATION_LEVEL);
268             javascriptContext.setLanguageVersion(Context.VERSION_1_8);
269
270             script = javascriptContext.compileString(javascriptCode, subjectKey.getId(), 1, null);
271         } catch (Exception e) {
272             Context.exit();
273             throw new StateMachineException(
274                 "logic failed to compile for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
275         }
276     }
277
278     private boolean executeScript(final Object executionContext) throws StateMachineException {
279         Object returnObject = null;
280
281         try {
282             // Pass the subject context to the Javascript engine
283             Scriptable javascriptScope = javascriptContext.initStandardObjects();
284             javascriptScope.put("executor", javascriptScope, executionContext);
285
286             // Run the script
287             returnObject = script.exec(javascriptContext, javascriptScope);
288         } catch (final Exception e) {
289             throw new StateMachineException(
290                 "logic failed to run for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
291         }
292
293         if (!(returnObject instanceof Boolean)) {
294             throw new StateMachineException(
295                 "execute: logic for " + subjectKey.getId() + " returned a non-boolean value " + returnObject);
296         }
297
298         return (boolean) returnObject;
299     }
300
301     private void checkAndThrowExecutorException() throws StateMachineException {
302         StateMachineException exceptionToThrow = executorException.getAndSet(null);
303         if (exceptionToThrow != null) {
304             throw exceptionToThrow;
305         }
306     }
307 }