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
   *
   *      https://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.tools.ant.taskdefs.optional.script;
  
  import java.io.File;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  import org.apache.tools.ant.AntTypeDefinition;
  import org.apache.tools.ant.BuildException;
  import org.apache.tools.ant.ComponentHelper;
  import org.apache.tools.ant.MagicNames;
  import org.apache.tools.ant.Project;
  import org.apache.tools.ant.ProjectHelper;
  import org.apache.tools.ant.taskdefs.DefBase;
  import org.apache.tools.ant.types.ResourceCollection;
  import org.apache.tools.ant.util.ClasspathUtils;
  import org.apache.tools.ant.util.ScriptManager;
  import org.apache.tools.ant.util.ScriptRunnerBase;
  import org.apache.tools.ant.util.ScriptRunnerHelper;
  
  /**
   * Defines a task using a script.
   *
   * @since Ant 1.6
   */
  public class ScriptDef extends DefBase {
      /**
       * script runner helper
       */
      private ScriptRunnerHelper helper = new ScriptRunnerHelper();
  
      /** the name by which this script will be activated */
      private String name;
  
      /** Attributes definitions of this script */
      private List<Attribute> attributes = new ArrayList<>();
  
      /** Nested Element definitions of this script */
      private List<NestedElement> nestedElements = new ArrayList<>();
  
      /** The attribute names as a set */
      private Set<String> attributeSet;
  
      /** The nested element definitions indexed by their names */
      private Map<String, NestedElement> nestedElementMap;
  
      /**
       * Create a new {@link ScriptDef}.
       */
      public ScriptDef() {
          helper.setSetBeans(false);
      }
  
      /**
       * Set the project.
       * @param project the project that this definition belongs to.
       */
      @Override
      public void setProject(Project project) {
          super.setProject(project);
          helper.setProjectComponent(this);
      }
  
      /**
       * Sets the name under which this script will be activated in a build
       * file
       *
       * @param name the name of the script
       */
      public void setName(String name) {
          this.name = name;
      }
  
      /**
       * Indicates whether the task supports a given attribute name
       *
       * @param attributeName the name of the attribute.
       *
       * @return true if the attribute is supported by the script.
       */
      public boolean isAttributeSupported(String attributeName) {
          return attributeSet.contains(attributeName);
      }
  
      /**
       * Class representing an attribute definition
       */
      public static class Attribute {
          /** The attribute name */
          private String name;
  
          /** The attribute's default value */
          private String defaultValue;
  
          /**
           * Sets the attribute name.
           *
           * @param name the attribute name
           */
          public void setName(String name) {
              this.name = name.toLowerCase(Locale.ENGLISH);
          }
  
          /**
           * Get the name of this {@link Attribute}.
           * 
           * @return {@link String}
           */
          String getName() {
              return name;
          }
  
          /**
           * Set the default value of this {@link Attribute}.
           *
           * @param defaultValue {@link String}
           * @since Ant 1.10.13
           */
          public void setDefault(String defaultValue) {
              this.defaultValue = defaultValue;
          }
  
          /**
           * Get the default value of this {@link Attribute}, {@code null} if
           * unset.
           * 
           * @return {@link String}
           * @since Ant 1.10.13
           */
          String getDefault() {
              return defaultValue;
          }
  
          /**
           * Learn whether this {@link Attribute} has a default value set.
           * @return {@code boolean}
           */
          boolean hasDefault() {
              return defaultValue != null;
          }
      }
  
      /**
       * Adds an attribute definition to this script.
       *
       * @param attribute the attribute definition.
       */
      public void addAttribute(Attribute attribute) {
          attributes.add(attribute);
      }
  
      /**
       * Class to represent a nested element definition
       */
      public static class NestedElement {
          /** The name of the nested element */
          private String name;
  
          /** The Ant type to which this nested element corresponds. */
          private String type;
  
          /** The class to be created for this nested element */
          private String className;
  
          /**
           * Sets the tag name for this nested element
           *
           * @param name the name of this nested element
           */
          public void setName(String name) {
              this.name = name.toLowerCase(Locale.ENGLISH);
          }
  
          /**
           * Sets the type of this element. This is the name of an
           * Ant task or type which is to be used when this element is to be
           * created. This is an alternative to specifying the class name directly
           *
           * @param type the name of an Ant type, or task, to use for this nested
           * element.
           */
          public void setType(String type) {
              this.type = type;
          }
  
          /**
           * Sets the classname of the class to be used for the nested element.
           * This specifies the class directly and is an alternative to specifying
           * the Ant type name.
           *
           * @param className the name of the class to use for this nested
           * element.
           */
          public void setClassName(String className) {
              this.className = className;
          }
      }
  
      /**
       * Adds a nested element definition.
       *
       * @param nestedElement the nested element definition.
       */
      public void addElement(NestedElement nestedElement) {
          nestedElements.add(nestedElement);
      }
  
      /**
       * Defines the script.
       */
      @Override
      public void execute() {
          if (name == null) {
              throw new BuildException(
                  "scriptdef requires a name attribute to name the script");
          }
  
          if (helper.getLanguage() == null) {
              throw new BuildException(
                  "scriptdef requires a language attribute to specify the script language");
          }
  
          if (helper.getSrc() == null && helper.getEncoding() != null) {
              throw new BuildException(
                  "scriptdef requires a src attribute if the encoding is set");
          }
  
          // Check if need to set the loader
          if (getAntlibClassLoader() != null || hasCpDelegate()) {
              helper.setClassLoader(createLoader());
          }
  
          attributeSet = new HashSet<>();
          for (Attribute attribute : attributes) {
              if (attribute.name == null) {
                  throw new BuildException(
                      "scriptdef <attribute> elements must specify an attribute name");
              }
              if (attributeSet.contains(attribute.name)) {
                  throw new BuildException(
                      "scriptdef <%s> declares the %s attribute more than once",
                      name, attribute.name);
              }
              attributeSet.add(attribute.name);
          }
  
          nestedElementMap = new HashMap<>();
          for (NestedElement nestedElement : nestedElements) {
              if (nestedElement.name == null) {
                  throw new BuildException(
                      "scriptdef <element> elements must specify an element name");
              }
              if (nestedElementMap.containsKey(nestedElement.name)) {
                  throw new BuildException(
                      "scriptdef <%s> declares the %s nested element more than once",
                      name, nestedElement.name);
              }
  
              if (nestedElement.className == null
                  && nestedElement.type == null) {
                  throw new BuildException(
                      "scriptdef <element> elements must specify either a classname or type attribute");
              }
              if (nestedElement.className != null
                  && nestedElement.type != null) {
                  throw new BuildException(
                      "scriptdef <element> elements must specify only one of the classname and type attributes");
              }
              nestedElementMap.put(nestedElement.name, nestedElement);
          }
  
          // find the script repository - it is stored in the project
          Map<String, ScriptDef> scriptRepository = lookupScriptRepository();
          name = ProjectHelper.genComponentName(getURI(), name);
          scriptRepository.put(name, this);
          AntTypeDefinition def = new AntTypeDefinition();
          def.setName(name);
          def.setClass(ScriptDefBase.class);
          ComponentHelper.getComponentHelper(
              getProject()).addDataTypeDefinition(def);
      }
  
      /**
       * Finds or creates the script repository - it is stored in the project.
       * This method is synchronized on the project under {@link MagicNames#SCRIPT_REPOSITORY}
       * @return the current script repository registered as a reference.
       */
      private Map<String, ScriptDef> lookupScriptRepository() {
          Map<String, ScriptDef> scriptRepository;
          Project p = getProject();
          synchronized (p) {
              scriptRepository =
                      p.getReference(MagicNames.SCRIPT_REPOSITORY);
              if (scriptRepository == null) {
                  scriptRepository = new HashMap<>();
                  p.addReference(MagicNames.SCRIPT_REPOSITORY,
                          scriptRepository);
              }
          }
          return scriptRepository;
      }
  
      /**
       * Creates a nested element to be configured.
       *
       * @param elementName the name of the nested element.
       * @return object representing the element name.
       */
      public Object createNestedElement(String elementName) {
          NestedElement definition = nestedElementMap.get(elementName);
          if (definition == null) {
              throw new BuildException(
                  "<%s> does not support the <%s> nested element", name,
                  elementName);
          }
  
          Object instance;
          String classname = definition.className;
          if (classname == null) {
              instance = getProject().createTask(definition.type);
              if (instance == null) {
                  instance = getProject().createDataType(definition.type);
              }
          } else {
              ClassLoader loader = createLoader();
  
              try {
                  instance = ClasspathUtils.newInstance(classname, loader);
              } catch (BuildException e) {
                  instance = ClasspathUtils.newInstance(classname, ScriptDef.class.getClassLoader());
              }
              getProject().setProjectReference(instance);
          }
  
          if (instance == null) {
              throw new BuildException(
                  "<%s> is unable to create the <%s> nested element", name,
                  elementName);
          }
          return instance;
      }
  
      /**
       * Executes the script.
       *
       * @param attributes collection of attributes
       * @param elements a list of nested element values.
       * @deprecated since 1.7.
       *             Use executeScript(attribute, elements, instance) instead.
       */
      @Deprecated
      public void executeScript(Map<String, String> attributes,
          Map<String, List<Object>> elements) {
          executeScript(attributes, elements, null);
      }
  
      /**
       * Executes the script.
       * This is called by the script instance to execute the script for this
       * definition.
       *
       * @param attributes collection of attributes
       * @param elements   a list of nested element values.
       * @param instance   the script instance; can be null
       */
      public void executeScript(Map<String, String> attributes,
          Map<String, List<Object>> elements, ScriptDefBase instance) {
          ScriptRunnerBase runner = helper.getScriptRunner();
  
          runner.addBean("attributes", withDefault(attributes));
          runner.addBean("elements", elements);
          runner.addBean("project", getProject());
          if (instance != null) {
              runner.addBean("self", instance);
          }
          runner.executeScript("scriptdef_" + name);
      }
  
      /**
       * Defines the manager.
       *
       * @param manager the scripting manager.
       */
      @Deprecated
      public void setManager(String manager) {
          helper.setManager(manager);
      }
  
      /**
       * Set the script manager.
       * @param manager
       */
      public void setManager(ScriptManager manager) {
          helper.setManager(manager);
      }
  
      /**
       * Defines the language (required).
       *
       * @param language the scripting language name for the script.
       */
      public void setLanguage(String language) {
          helper.setLanguage(language);
      }
  
      /**
       * Defines the compilation feature; optional.
       *
       * @param compiled enables the script compilation if available.
       * @since Ant 1.10.2
       */
      public void setCompiled(boolean compiled) {
          helper.setCompiled(compiled);
      }
  
      /**
       * Loads the script from an external file; optional.
       *
       * @param file the file containing the script source.
       */
      public void setSrc(File file) {
          helper.setSrc(file);
      }
  
      /**
       * Sets the encoding of the script from an external file; optional.
       *
       * @param encoding the encoding of the file containing the script source.
       * @since Ant 1.10.2
       */
      public void setEncoding(String encoding) {
          helper.setEncoding(encoding);
      }
  
      /**
       * Set the setbeans attribute.
       * If this is true, &lt;script&gt; will create variables in the
       * script instance for all
       * properties, targets and references of the current project.
       * It this is false, only the project and self variables will
       * be set.
       * The default is true.
       * @param setBeans the value to set.
       */
      public void setSetBeans(boolean setBeans) {
          helper.setSetBeans(setBeans);
      }
  
      /**
       * Sets the script text.
       *
       * @param text a component of the script text to be added.
       */
      public void addText(String text) {
          helper.addText(text);
      }
  
      /**
       * Adds any source resource.
       * @since Ant 1.7.1
       * @param resource source of script
       */
      public void add(ResourceCollection resource) {
          helper.add(resource);
      }
  
      private Map<String, String> withDefault(Map<String, String> attributes) {
          /*
           * The following is checked in ScriptDefBase but some other caller to
           * #executeScript() could pass in anything they like
           */
          final Set<String> unsupported =
              attributeSet.stream().filter(a -> !this.isAttributeSupported(a)).map(s -> '@' + s)
                  .collect(Collectors.toSet());
  
          if (!unsupported.isEmpty()) {
              throw new BuildException("Found unsupported attributes " + unsupported);
          }
          if (this.attributes.isEmpty()) {
              return attributes;
          }
          final Map<String, String> result =
              this.attributes.stream().filter(Attribute::hasDefault).collect(Collectors
                  .toMap(Attribute::getName, Attribute::getDefault, (l, r) -> r, LinkedHashMap::new));
  
          result.putAll(attributes);
  
          return result;
      }
  }
  