content: // Licensed to the Apache Software Foundation (ASF) under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  The ASF licenses this file
  // to you under the Apache License, Version 2.0 (the
  // "License"); you may not use this file except in compliance
  // with the License.  You may obtain a copy of the License at
  //
  //   http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing,
  // software distributed under the License is distributed on an
  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  // KIND, either express or implied.  See the License for the
  // specific language governing permissions and limitations
  // under the License.
  
  package org.apache.cloudstack.jsinterpreter;
  
  import com.fasterxml.jackson.core.JsonProcessingException;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.apache.commons.lang3.StringUtils;
  import org.apache.logging.log4j.Logger;
  import org.apache.logging.log4j.LogManager;
  import org.openjdk.nashorn.api.scripting.ScriptUtils;
  import org.openjdk.nashorn.internal.runtime.Context;
  import org.openjdk.nashorn.internal.runtime.ErrorManager;
  import org.openjdk.nashorn.internal.runtime.options.Options;
  
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.Map;
  import java.util.Set;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  public class JsInterpreterHelper {
      private final Logger logger = LogManager.getLogger(getClass());
  
      private static final String NAME = "name";
      private static final String PROPERTY = "property";
      private static final String TYPE = "type";
      private static final String CALL_EXPRESSION = "CallExpression";
  
      private int callExpressions;
  
      private StringBuilder variable;
  
      private Set<String> variables;
  
      /**
       * Returns all variables from the given script.
       *
       * @param script the script to extract the variables.
       * @return A {@link Set<String>} containing all variables in the script.
       */
      public Set<String> getScriptVariables(String script) {
          String parseTree = getScriptAsJsonTree(script);
          ObjectMapper mapper = new ObjectMapper();
          JsonNode jsonNode = null;
          variables = new HashSet<>();
          variable = new StringBuilder();
  
          try {
              jsonNode = mapper.readTree(parseTree);
          } catch (JsonProcessingException e) {
              logger.error("Unable to create the script JSON tree due to: [{}].", e.getMessage(), e);
          }
  
          logger.trace("Searching script variables from [{}].", script);
          iterateOverJsonTree(jsonNode.fields());
  
          if (StringUtils.isNotBlank(variable.toString())) {
              logger.trace("Adding variable [{}] into the variables set.", variable);
              removeCallFunctionsFromVariable();
              variables.add(variable.toString());
          }
  
          logger.trace("Found the following variables from the given script: [{}]", variables);
          return variables;
      }
  
      private String getScriptAsJsonTree(String script) {
          logger.trace("Creating JSON Tree for script [{}].", script);
          Options options = new Options("nashorn");
          options.set("anon.functions", true);
          options.set("parse.only", true);
          options.set("scripting", true);
  
          ErrorManager errors = new ErrorManager();
          Context context = new Context(options, errors, Thread.currentThread().getContextClassLoader());
          Context.setGlobal(context.createGlobal());
  
          return ScriptUtils.parse(script, "nashorn", false);
      }
  
      protected void iterateOverJsonTree(Iterator<Map.Entry<String, JsonNode>> iterator) {
          while (iterator.hasNext()) {
              iterateOverJsonTree(iterator.next());
          }
      }
  
      protected void iterateOverJsonTree(Map.Entry<String, JsonNode> fields) {
          JsonNode node = null;
  
          if (fields.getValue().isArray()) {
              iterateOverArrayNodes(fields);
          } else {
              node = fields.getValue();
          }
  
          String fieldName = searchIntoObjectNodes(node);
  
          if (fieldName == null) {
              String key = fields.getKey();
              if (TYPE.equals(key) && CALL_EXPRESSION.equals(node.textValue())) {
                  callExpressions++;
              }
  
              if (NAME.equals(key) || PROPERTY.equals(key)) {
                  appendFieldValueToVariable(key, node);
              }
          }
      }
  
      protected void iterateOverArrayNodes(Map.Entry<String, JsonNode> fields) {
          for (int count = 0; fields.getValue().get(count) != null; count++) {
              iterateOverJsonTree(fields.getValue().get(count).fields());
          }
      }
  
      protected String searchIntoObjectNodes(JsonNode node) {
          if (node == null) {
              return null;
          }
  
          String fieldName = null;
          Iterator<String> iterator = node.fieldNames();
          while (iterator.hasNext()) {
              fieldName = iterator.next();
              if (TYPE.equals(fieldName) && CALL_EXPRESSION.equals(node.get(fieldName).textValue())) {
                  callExpressions++;
              }
  
              if (NAME.equals(fieldName) || PROPERTY.equals(fieldName)) {
                  appendFieldValueToVariable(fieldName, node.get(fieldName));
              }
  
              if (node.get(fieldName).isArray()) {
                  JsonNode blockStatementContent = node.get(fieldName).get(0);
                  if (blockStatementContent != null) {
                      iterateOverJsonTree(blockStatementContent.fields());
                  }
              } else {
                  iterateOverJsonTree(node.get(fieldName).fields());
              }
          }
  
          return fieldName;
      }
  
      protected void appendFieldValueToVariable(String key, JsonNode node) {
          String nodeTextValue = node.textValue();
          if (nodeTextValue == null) {
              return;
          }
  
          if (PROPERTY.equals(key)) {
              logger.trace("Appending field value [{}] to variable [{}] as the field name is \"property\".", nodeTextValue, variable);
              variable.append(".").append(nodeTextValue);
              return;
          }
  
          logger.trace("Building new variable [{}] as the field name is \"name\"", nodeTextValue);
          if (StringUtils.isNotBlank(variable.toString())) {
              logger.trace("Adding variable [{}] into the variables set.", variable);
              removeCallFunctionsFromVariable();
              variables.add(variable.toString());
              variable.setLength(0);
          }
          variable.append(nodeTextValue);
      }
  
      protected void removeCallFunctionsFromVariable() {
          String[] disassembledVariable = variable.toString().split("\\.");
          variable.setLength(0);
  
          int newVariableSize = disassembledVariable.length - callExpressions;
          String[] newVariable = Arrays.copyOfRange(disassembledVariable, 0, newVariableSize);
  
          variable.append(String.join(".", newVariable));
          callExpressions = 0;
      }
  
      /**
       * Replaces all variables in script that matches the key in {@link Map} for their respective values.
       *
       * @param script the script which the variables will be replaced.
       * @param variablesToReplace a {@link Map} which has the key as the variable to be replaced and the value as the variable to replace.
       * @return A new script with the variables replaced.
       */
      public String replaceScriptVariables(String script, Map<String, String> variablesToReplace) {
          String regex = String.format("\\b(%s)\\b", String.join("|", variablesToReplace.keySet()));
          Matcher matcher = Pattern.compile(regex).matcher(script);
  
          StringBuilder sb = new StringBuilder();
          while (matcher.find()) {
              matcher.appendReplacement(sb, variablesToReplace.get(matcher.group()));
          }
          matcher.appendTail(sb);
  
          return sb.toString();
      }
  
      public int getCallExpressions() {
          return callExpressions;
      }
  
      public void setCallExpressions(int callExpressions) {
          this.callExpressions = callExpressions;
      }
  
      public StringBuilder getVariable() {
          return variable;
      }
  
      public void setVariable(StringBuilder variable) {
          this.variable = variable;
      }
  
      public Set<String> getVariables() {
          return variables;
      }
  
      public void setVariables(Set<String> variables) {
          this.variables = variables;
      }
  }
  