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.camel.language.groovy;
  
  import java.util.HashMap;
  import java.util.Map;
  
  import groovy.lang.Binding;
  import groovy.lang.GroovyShell;
  import groovy.lang.Script;
  import org.apache.camel.Exchange;
  import org.apache.camel.Ordered;
  import org.apache.camel.Service;
  import org.apache.camel.spi.CamelEvent;
  import org.apache.camel.spi.EventNotifier;
  import org.apache.camel.spi.ScriptingLanguage;
  import org.apache.camel.spi.annotations.Language;
  import org.apache.camel.support.DefaultExchange;
  import org.apache.camel.support.LRUCacheFactory;
  import org.apache.camel.support.ObjectHelper;
  import org.apache.camel.support.SimpleEventNotifierSupport;
  import org.apache.camel.support.TypedLanguageSupport;
  import org.apache.camel.support.service.ServiceHelper;
  import org.codehaus.groovy.runtime.InvokerHelper;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  @Language("groovy")
  public class GroovyLanguage extends TypedLanguageSupport implements ScriptingLanguage, Service {
  
      private static final Logger LOG = LoggerFactory.getLogger(GroovyLanguage.class);
  
      static final int RELOAD_ORDER = 100;
  
      /**
       * In case the expression is referring to an external resource, it indicates whether it is still needed to load the
       * resource.
       */
      private final boolean loadExternalResource;
  
      /**
       * Cache used to stores the compiled scripts (aka their classes)
       */
      private final Map<String, GroovyClassService> scriptCache;
  
      private EventNotifier notifier;
  
      private GroovyLanguage(Map<String, GroovyClassService> scriptCache, boolean loadExternalResource) {
          this.scriptCache = scriptCache;
          this.loadExternalResource = loadExternalResource;
      }
  
      public GroovyLanguage() {
          this(LRUCacheFactory.newLRUSoftCache(16, 1000, true), true);
      }
  
      @Override
      public void start() {
          // are we in dev mode then support flushing cache on reload
          if (getCamelContext() != null) {
              String profile = getCamelContext().getCamelContextExtension().getProfile();
              if ("dev".equals(profile)) {
                  if (notifier == null) {
                      notifier = new ReloadNotifier();
                      getCamelContext().getManagementStrategy().addEventNotifier(notifier);
                  }
              }
          }
      }
  
      @Override
      public void stop() {
          ServiceHelper.stopService(scriptCache.values());
          scriptCache.clear();
          if (notifier != null) {
              getCamelContext().getManagementStrategy().removeEventNotifier(notifier);
              notifier = null;
          }
      }
  
      private final class ReloadNotifier extends SimpleEventNotifierSupport implements Ordered {
  
          @Override
          public void notify(CamelEvent event) throws Exception {
              // if context or route is reloading then clear cache to ensure old scripts are removed from memory.
              if (event instanceof CamelEvent.CamelContextReloadingEvent || event instanceof CamelEvent.RouteReloadedEvent) {
                  ServiceHelper.stopService(scriptCache.values());
                  scriptCache.clear();
              }
          }
  
          @Override
          public int getOrder() {
              return RELOAD_ORDER;
          }
      }
  
      private static final class GroovyClassService implements Service {
  
          private final Class<Script> script;
  
          private GroovyClassService(Class<Script> script) {
              this.script = script;
          }
  
          @Override
          public void start() {
          }
  
          @Override
          public void stop() {
              InvokerHelper.removeClass(script);
          }
  
      }
  
      public static GroovyExpression groovy(String expression) {
          return new GroovyLanguage().createExpression(expression);
      }
  
      @Override
      public GroovyExpression createPredicate(String expression) {
          return createExpression(expression);
      }
  
      @Override
      public GroovyExpression createExpression(String expression) {
          expression = loadResource(expression);
          return new GroovyExpression(expression);
      }
  
      @Override
      @SuppressWarnings("unchecked")
      public <T> T evaluate(String script, Map<String, Object> bindings, Class<T> resultType) {
          if (loadExternalResource) {
              script = loadResource(script);
          }
          Class<Script> clazz = getScriptFromCache(script);
          if (clazz == null) {
              // prefer to use classloader from groovy script compiler, and if not fallback to app context
              ClassLoader cl = getCamelContext().getCamelContextExtension().getContextPlugin(GroovyScriptClassLoader.class);
              GroovyShell shell = cl != null ? new GroovyShell(cl) : new GroovyShell();
              clazz = shell.getClassLoader().parseClass(script);
              addScriptToCache(script, clazz);
          }
          Script gs = ObjectHelper.newInstance(clazz, Script.class);
          if (bindings != null) {
              gs.setBinding(new Binding(bindings));
          }
          gs.getBinding().setVariable("log", LOG);
          Object value = gs.run();
          return getCamelContext().getTypeConverter().convertTo(resultType, value);
      }
  
      // use by tooling
      public boolean validateExpression(String expression) throws GroovyValidationException {
          final Exchange dummy = new DefaultExchange(getCamelContext());
          Map<String, Object> globalVariables = new HashMap<>();
  
          try {
              GroovyExpression ge = createExpression(expression);
              Script script = ge.instantiateScript(dummy, globalVariables);
              script.setBinding(ge.createBinding(dummy, globalVariables));
              script.run();
          } catch (Exception e) {
              throw new GroovyValidationException(expression, e);
          }
          return true;
      }
  
      // use by tooling
      public boolean validatePredicate(String expression) throws GroovyValidationException {
          return validateExpression(expression);
      }
  
      Class<Script> getScriptFromCache(String script) {
          final GroovyClassService cached = scriptCache.get(script);
          if (cached == null) {
              return null;
          }
          return cached.script;
      }
  
      void addScriptToCache(String script, Class<Script> scriptClass) {
          scriptCache.put(script, new GroovyClassService(scriptClass));
      }
  
      public static class Builder {
          private final Map<String, GroovyClassService> cache = new HashMap<>();
  
          public void addScript(String content, Class<Script> scriptClass) {
              cache.put(content, new GroovyClassService(scriptClass));
          }
  
          public GroovyLanguage build() {
              return new GroovyLanguage(cache, false);
          }
      }
  }
  