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.ivy.core.settings;
  
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.MalformedURLException;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.net.URL;
  import java.text.ParseException;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import org.apache.ivy.core.IvyPatternHelper;
  import org.apache.ivy.core.cache.RepositoryCacheManager;
  import org.apache.ivy.core.module.status.StatusManager;
  import org.apache.ivy.plugins.circular.CircularDependencyStrategy;
  import org.apache.ivy.plugins.conflict.ConflictManager;
  import org.apache.ivy.plugins.latest.LatestStrategy;
  import org.apache.ivy.plugins.lock.LockStrategy;
  import org.apache.ivy.plugins.matcher.PatternMatcher;
  import org.apache.ivy.util.Checks;
  import org.apache.ivy.util.Configurator;
  import org.apache.ivy.util.FileResolver;
  import org.apache.ivy.util.Message;
  import org.apache.ivy.util.XMLHelper;
  import org.apache.ivy.util.url.CredentialsStore;
  import org.apache.ivy.util.url.TimeoutConstrainedURLHandler;
  import org.apache.ivy.util.url.URLHandlerRegistry;
  import org.xml.sax.Attributes;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  import org.xml.sax.helpers.DefaultHandler;
  
  import static org.apache.ivy.util.StringUtils.isNullOrEmpty;
  
  /**
   */
  public class XmlSettingsParser extends DefaultHandler {
      /**
       * Wraps an {@link IvyVariableContainer} delegating most method calls to the wrapped instance,
       * except for a set of variables which are only stored locally in the wrapper, and not
       * propagated to the wrapped instance.
       */
      private static final class IvyVariableContainerWrapper implements IvyVariableContainer {
          private static final Collection<String> SETTINGS_VARIABLES = Arrays.asList(
              "ivy.settings.dir", "ivy.settings.url", "ivy.settings.file", "ivy.conf.dir",
              "ivy.conf.url", "ivy.conf.file");
  
          private final IvyVariableContainer variables;
  
          private Map<String, String> localVariables = new HashMap<>();
  
          private IvyVariableContainerWrapper(IvyVariableContainer variables) {
              this.variables = variables;
          }
  
          public void setVariable(String varName, String value, boolean overwrite) {
              if (SETTINGS_VARIABLES.contains(varName)) {
                  if (!localVariables.containsKey(varName) || overwrite) {
                      localVariables.put(varName, value);
                  }
              } else {
                  variables.setVariable(varName, value, overwrite);
              }
          }
  
          public void setEnvironmentPrefix(String prefix) {
              variables.setEnvironmentPrefix(prefix);
          }
  
          public String getVariable(String name) {
              if (localVariables.containsKey(name)) {
                  return localVariables.get(name);
              }
              return variables.getVariable(name);
          }
  
          public Object clone() {
              throw new UnsupportedOperationException();
          }
      }
  
      private Configurator configurator;
  
      private List<String> configuratorTags = Arrays.asList("resolvers", "namespaces", "parsers",
          "latest-strategies", "conflict-managers", "outputters", "version-matchers", "statuses",
          "circular-dependency-strategies", "triggers", "lock-strategies", "caches", "signers", "timeout-constraints");
  
      private IvySettings ivy;
  
      private String defaultResolver;
  
      private String defaultCM;
  
      private String defaultLatest;
  
      private String defaultCacheManager;
  
      private String defaultCircular;
  
      private String defaultLock;
  
      private String currentConfiguratorTag;
  
      private URL settings;
  
      private boolean deprecatedMessagePrinted = false;
  
      public XmlSettingsParser(IvySettings ivy) {
          this.ivy = ivy;
      }
  
      public void parse(URL settings) throws ParseException, IOException {
          configurator = new Configurator();
          configurator.setFileResolver(new FileResolver() {
              public File resolveFile(String path, String filename) {
                  return Checks.checkAbsolute(path, filename);
              }
          });
          // put every type definition from ivy to configurator
          for (Map.Entry<String, Class<?>> entry : ivy.getTypeDefs().entrySet()) {
              configurator.typeDef(entry.getKey(), entry.getValue());
          }
  
          doParse(settings);
      }
  
      @SuppressWarnings("deprecation")
      private void doParse(URL settingsUrl) throws IOException, ParseException {
          this.settings = settingsUrl;
          try {
              XMLHelper.parse(settingsUrl, null, this);
              ivy.validate();
          } catch (IOException e) {
              throw e;
          } catch (Exception e) {
              ParseException pe = new ParseException("failed to load settings from " + settingsUrl
                      + ": " + e.getMessage(), 0);
              pe.initCause(e);
              throw pe;
          }
      }
  
      private void parse(Configurator configurator, URL configuration) throws IOException,
              ParseException {
          this.configurator = configurator;
          doParse(configuration);
      }
  
      @Override
      public void startElement(String uri, String localName, String qName, Attributes att)
              throws SAXException {
          // we first copy attributes in a Map to be able to modify them
          Map<String, String> attributes = new HashMap<>();
          for (int i = 0; i < att.getLength(); i++) {
              attributes.put(att.getQName(i), ivy.substitute(att.getValue(i)));
          }
  
          try {
              if ("ivyconf".equals(qName)) {
                  deprecatedMessagePrinted = true;
                  Message.deprecated("'ivyconf' element is deprecated, use 'ivysettings' instead ("
                          + settings + ")");
              }
              if (configurator.getCurrent() != null) {
                  inConfiguratorStarted(qName, attributes);
              } else if ("classpath".equals(qName)) {
                  classpathStarted(attributes);
              } else if ("typedef".equals(qName)) {
                  typedefStarted(attributes);
              } else if ("property".equals(qName)) {
                  propertyStarted(attributes);
              } else if ("properties".equals(qName)) {
                  propertiesStarted(attributes);
              } else if ("include".equals(qName)) {
                  includeStarted(attributes);
              } else if ("settings".equals(qName) || "conf".equals(qName)) {
                  settingsStarted(qName, attributes);
              } else if ("caches".equals(qName)) {
                  cachesStarted(qName, attributes);
              } else if ("version-matchers".equals(qName)) {
                  versionMatchersStarted(qName, attributes);
              } else if ("statuses".equals(qName)) {
                  statusesStarted(qName, attributes);
              } else if (configuratorTags.contains(qName)) {
                  anyConfiguratorStarted(qName);
              } else if ("macrodef".equals(qName)) {
                  macrodefStarted(qName, attributes);
              } else if ("module".equals(qName)) {
                  moduleStarted(attributes);
              } else if ("credentials".equals(qName)) {
                  credentialsStarted(attributes);
              }
          } catch (ParseException ex) {
              throw new SAXException("problem in config file: " + ex.getMessage(), ex);
          } catch (IOException ex) {
              throw new SAXException("io problem while parsing config file: " + ex.getMessage(), ex);
          }
      }
  
      private void credentialsStarted(Map<String, String> attributes) {
          String realm = attributes.remove("realm");
          String host = attributes.remove("host");
          String userName = attributes.remove("username");
          String passwd = attributes.remove("passwd");
          CredentialsStore.INSTANCE.addCredentials(realm, host, userName, passwd);
      }
  
      private void moduleStarted(Map<String, String> attributes) {
          attributes.put(IvyPatternHelper.MODULE_KEY, attributes.remove("name"));
          String resolver = attributes.remove("resolver");
          String branch = attributes.remove("branch");
          String cm = attributes.remove("conflict-manager");
          String resolveMode = attributes.remove("resolveMode");
          String matcher = attributes.remove("matcher");
          matcher = (matcher == null) ? PatternMatcher.EXACT_OR_REGEXP : matcher;
          ivy.addModuleConfiguration(attributes, ivy.getMatcher(matcher), resolver, branch, cm,
              resolveMode);
      }
  
      private void macrodefStarted(String qName, Map<String, String> attributes) {
          currentConfiguratorTag = qName;
          Configurator.MacroDef macrodef = configurator.startMacroDef(attributes.get("name"));
          macrodef.addAttribute("name", null);
      }
  
      private void anyConfiguratorStarted(String qName) {
          currentConfiguratorTag = qName;
          configurator.setRoot(ivy);
      }
  
      private void statusesStarted(String qName, Map<String, String> attributes) {
          currentConfiguratorTag = qName;
          StatusManager m = new StatusManager();
          String defaultStatus = attributes.get("default");
          if (defaultStatus != null) {
              m.setDefaultStatus(defaultStatus);
          }
          ivy.setStatusManager(m);
          configurator.setRoot(m);
      }
  
      private void versionMatchersStarted(String qName, Map<String, String> attributes) {
          anyConfiguratorStarted(qName);
          if ("true".equals(attributes.get("usedefaults"))) {
              ivy.configureDefaultVersionMatcher();
          }
      }
  
      private void cachesStarted(String qName, Map<String, String> attributes) {
          anyConfiguratorStarted(qName);
          defaultLock = attributes.get("lockStrategy");
          defaultCacheManager = attributes.get("default");
  
          String cache = attributes.get("defaultCacheDir");
          if (cache != null) {
              ivy.setDefaultCache(Checks.checkAbsolute(cache, "defaultCacheDir"));
          }
          String up2d = attributes.get("checkUpToDate");
          if (up2d != null) {
              Message.deprecated("'checkUpToDate' is deprecated, "
                      + "use the 'overwriteMode' on the 'ivy:retrieve' task instead (" + settings
                      + ")");
              ivy.setCheckUpToDate(Boolean.valueOf(up2d));
          }
          String resolutionDir = attributes.get("resolutionCacheDir");
          if (resolutionDir != null) {
              ivy.setDefaultResolutionCacheBasedir(resolutionDir);
          }
          String useOrigin = attributes.get("useOrigin");
          if (useOrigin != null) {
              ivy.setDefaultUseOrigin(Boolean.valueOf(useOrigin));
          }
          String cacheIvyPattern = attributes.get("ivyPattern");
          if (cacheIvyPattern != null) {
              ivy.setDefaultCacheIvyPattern(cacheIvyPattern);
          }
          String cacheArtPattern = attributes.get("artifactPattern");
          if (cacheArtPattern != null) {
              ivy.setDefaultCacheArtifactPattern(cacheArtPattern);
          }
          String repositoryDir = attributes.get("repositoryCacheDir");
          if (repositoryDir != null) {
              ivy.setDefaultRepositoryCacheBasedir(repositoryDir);
          }
      }
  
      @SuppressWarnings("deprecation")
      private void settingsStarted(String qName, Map<String, String> attributes) {
          if ("conf".equals(qName) && !deprecatedMessagePrinted) {
              Message.deprecated("'conf' is deprecated, use 'settings' instead (" + settings + ")");
          }
          String cache = attributes.get("defaultCache");
          if (cache != null) {
              Message.deprecated("'defaultCache' is deprecated, "
                      + "use 'caches[@defaultCacheDir]' instead (" + settings + ")");
              ivy.setDefaultCache(Checks.checkAbsolute(cache, "defaultCache"));
          }
          String defaultBranch = attributes.get("defaultBranch");
          if (defaultBranch != null) {
              ivy.setDefaultBranch(defaultBranch);
          }
          String defaultResolveMode = attributes.get("defaultResolveMode");
          if (defaultResolveMode != null) {
              ivy.setDefaultResolveMode(defaultResolveMode);
          }
          String validate = attributes.get("validate");
          if (validate != null) {
              ivy.setValidate(Boolean.valueOf(validate));
          }
          String up2d = attributes.get("checkUpToDate");
          if (up2d != null) {
              Message.deprecated("'checkUpToDate' is deprecated, "
                      + "use the 'overwriteMode' on the 'ivy:retrieve' task instead (" + settings
                      + ")");
              ivy.setCheckUpToDate(Boolean.valueOf(up2d));
          }
          String useRemoteConfig = attributes.get("useRemoteConfig");
          if (useRemoteConfig != null) {
              ivy.setUseRemoteConfig(Boolean.valueOf(useRemoteConfig));
          }
          String cacheIvyPattern = attributes.get("cacheIvyPattern");
          if (cacheIvyPattern != null) {
              Message.deprecated("'cacheIvyPattern' is deprecated, use 'caches[@ivyPattern]' instead"
                      + " (" + settings + ")");
              ivy.setDefaultCacheIvyPattern(cacheIvyPattern);
          }
          String cacheArtPattern = attributes.get("cacheArtifactPattern");
          if (cacheArtPattern != null) {
              Message.deprecated("'cacheArtifactPattern' is deprecated, "
                      + "use 'caches[@artifactPattern]' instead (" + settings + ")");
              ivy.setDefaultCacheArtifactPattern(cacheArtPattern);
          }
  
          // we do not set following defaults here since no instances has been registered yet
          defaultResolver = attributes.get("defaultResolver");
          defaultCM = attributes.get("defaultConflictManager");
          defaultLatest = attributes.get("defaultLatestStrategy");
          defaultCircular = attributes.get("circularDependencyStrategy");
  
          String requestMethod = attributes.get("httpRequestMethod");
          if ("head".equalsIgnoreCase(requestMethod)) {
              URLHandlerRegistry.getHttp().setRequestMethod(TimeoutConstrainedURLHandler.REQUEST_METHOD_HEAD);
          } else if ("get".equalsIgnoreCase(requestMethod)) {
              URLHandlerRegistry.getHttp().setRequestMethod(TimeoutConstrainedURLHandler.REQUEST_METHOD_GET);
          } else if (!isNullOrEmpty(requestMethod)) {
              throw new IllegalArgumentException(
                      "Invalid httpRequestMethod specified, must be one of {'HEAD', 'GET'}");
          }
      }
  
      private void includeStarted(Map<String, String> attributes) throws IOException, ParseException {
          final IvyVariableContainer variables = ivy.getVariableContainer();
          ivy.setVariableContainer(new IvyVariableContainerWrapper(variables));
          final boolean optionalInclude = "true".equals(attributes.get("optional"));
          try {
              String propFilePath = attributes.get("file");
              URL settingsURL = null;
              if (propFilePath == null) {
                  propFilePath = attributes.get("url");
                  if (propFilePath == null) {
                      throw new IllegalArgumentException(
                              "bad include tag: specify file or url to include");
                  } else {
                      try {
                          try {
                              // First assume that it is an absolute URL
                              settingsURL = new URL(propFilePath);
                          } catch (MalformedURLException e) {
                              // If that fail, it may be because it is a relative one.
                              settingsURL = new URL(this.settings, propFilePath);
                          }
                      } catch (IOException ioe) {
                          if (!optionalInclude) {
                              throw ioe;
                          }
                          Message.verbose("Skipping inclusion of optional URL " + propFilePath
                                  + " due to IOException - " + ioe.getMessage());
                          return;
                      }
                      Message.verbose("including url: " + settingsURL.toString());
                      ivy.setSettingsVariables(settingsURL);
                  }
              } else {
                  try {
                      settingsURL = urlFromFileAttribute(propFilePath);
                      Message.verbose("including file: " + settingsURL);
                      if ("file".equals(settingsURL.getProtocol())) {
                          try {
                              File settingsFile = new File(new URI(settingsURL.toExternalForm()));
                              if (optionalInclude && !settingsFile.exists()) {
                                  return;
                              }
                              ivy.setSettingsVariables(Checks.checkAbsolute(settingsFile,
                                      "settings include path"));
                          } catch (URISyntaxException e) {
                              // try to make the best of it...
                              ivy.setSettingsVariables(Checks.checkAbsolute(settingsURL.getPath(),
                                      "settings include path"));
                          }
                      } else {
                          ivy.setSettingsVariables(settingsURL);
                      }
                  } catch (IOException ioe) {
                      if (!optionalInclude) {
                          throw ioe;
                      }
                      Message.verbose("Skipping inclusion of optional file " + propFilePath
                              + " due to IOException - " + ioe.getMessage());
                      return;
                  }
              }
              try {
                  new XmlSettingsParser(ivy).parse(configurator, settingsURL);
              } catch (IOException ioe) {
                  if (!optionalInclude) {
                      throw ioe;
                  }
                  Message.verbose("Skipping inclusion of optional settings URL " + settingsURL
                          + " due to IOException - " + ioe.getMessage());
                  return;
              }
          } finally {
              ivy.setVariableContainer(variables);
          }
      }
  
      /**
       * Provide an URL referencing the given filepath. If filePath is an absolute path, then the
       * resulting URL point to a local file, otherwise, the filepath is evaluated relatively to the
       * URL of the current settings file (can be local file or remote URL).
       */
      private URL urlFromFileAttribute(String filePath) throws IOException {
          try {
              return new URL(filePath);
          } catch (MalformedURLException e) {
              // ignore, we'll try to create a correct URL below
          }
  
          File incFile = new File(filePath);
          if (incFile.isAbsolute()) {
              if (!incFile.exists()) {
                  throw new FileNotFoundException(incFile.getAbsolutePath());
              }
              return incFile.toURI().toURL();
          } else if ("file".equals(this.settings.getProtocol())) {
              try {
                  File settingsFile = new File(new URI(this.settings.toExternalForm()));
                  if (!settingsFile.exists()) {
                      throw new FileNotFoundException(settingsFile.getAbsolutePath());
                  }
                  return new File(settingsFile.getParentFile(), filePath).toURI().toURL();
              } catch (URISyntaxException e) {
                  return new URL(this.settings, filePath);
              }
          } else {
              return new URL(this.settings, filePath);
          }
      }
  
      private void propertiesStarted(Map<String, String> attributes) throws IOException {
          String propFilePath = attributes.get("file");
          String environmentPrefix = attributes.get("environment");
          if (propFilePath != null) {
              String overrideStr = attributes.get("override");
              boolean override = (overrideStr == null) || Boolean.valueOf(overrideStr);
              Message.verbose("loading properties: " + propFilePath);
              try {
                  URL fileUrl = urlFromFileAttribute(propFilePath);
                  ivy.loadProperties(fileUrl, override);
              } catch (FileNotFoundException e) {
                  Message.verbose("Unable to find property file: " + propFilePath);
              }
          } else if (environmentPrefix != null) {
              ivy.getVariableContainer().setEnvironmentPrefix(environmentPrefix);
          } else {
              throw new IllegalArgumentException("Didn't find a 'file' or 'environment' attribute "
                      + "on the 'properties' element");
          }
      }
  
      private void propertyStarted(Map<String, String> attributes) {
          String name = attributes.get("name");
          String value = attributes.get("value");
          String override = attributes.get("override");
          String isSetVar = attributes.get("ifset");
          String unlessSetVar = attributes.get("unlessset");
          if (name == null) {
              throw new IllegalArgumentException("missing attribute name on property tag");
          }
          if (value == null) {
              throw new IllegalArgumentException("missing attribute value on property tag");
          }
          ivy.setVariable(name, value, (override == null) || Boolean.valueOf(override), isSetVar,
              unlessSetVar);
      }
  
      private void typedefStarted(Map<String, String> attributes) {
          String name = attributes.get("name");
          String className = attributes.get("classname");
          Class<?> clazz = ivy.typeDef(name, className);
          configurator.typeDef(name, clazz);
      }
  
      private void classpathStarted(Map<String, String> attributes) throws IOException {
          String urlStr = attributes.get("url");
          URL url = null;
          if (urlStr == null) {
              String file = attributes.get("file");
              if (file == null) {
                  throw new IllegalArgumentException(
                          "either url or file should be given for classpath element");
              } else {
                  url = urlFromFileAttribute(file);
              }
          } else {
              url = new URL(urlStr);
          }
          ivy.addClasspathURL(url);
      }
  
      private void inConfiguratorStarted(String qName, Map<String, String> attributes) {
          if ("macrodef".equals(currentConfiguratorTag) && configurator.getTypeDef(qName) != null) {
              String name = attributes.get("name");
              if (name == null) {
                  attributes.put("name", "@{name}");
              } else if (name.contains("@{name}")) {
                  attributes.put("name", name);
              } else {
                  attributes.put("name", "@{name}-" + name);
              }
          }
          if (attributes.get("ref") != null) {
              if (attributes.size() != 1) {
                  throw new IllegalArgumentException("ref attribute should be the only one ! found "
                          + attributes.size() + " in " + qName);
              }
              String name = attributes.get("ref");
              Object child = null;
              if ("resolvers".equals(currentConfiguratorTag) || "resolver".equals(qName)) {
                  child = ivy.getResolver(name);
                  if (child == null) {
                      throw new IllegalArgumentException("unknown resolver " + name
                              + ": resolver should be defined before being referenced");
                  }
              } else if ("latest-strategies".equals(currentConfiguratorTag)) {
                  child = ivy.getLatestStrategy(name);
                  if (child == null) {
                      throw new IllegalArgumentException("unknown latest strategy " + name
                              + ": latest strategy should be defined before being referenced");
                  }
              } else if ("conflict-managers".equals(currentConfiguratorTag)) {
                  child = ivy.getConflictManager(name);
                  if (child == null) {
                      throw new IllegalArgumentException("unknown conflict manager " + name
                              + ": conflict manager should be defined before being referenced");
                  }
              }
              if (child == null) {
                  throw new IllegalArgumentException("bad reference " + name);
              }
              configurator.addChild(qName, child);
          } else {
              configurator.startCreateChild(qName);
              for (Map.Entry<String, String> attribute : attributes.entrySet()) {
                  configurator.setAttribute(attribute.getKey(), attribute.getValue());
              }
          }
      }
  
      @Override
      public void endElement(String uri, String localName, String qName) throws SAXException {
          if (configurator.getCurrent() != null) {
              if (configuratorTags.contains(qName) && configurator.getDepth() == 1) {
                  configurator.clear();
                  currentConfiguratorTag = null;
              } else if ("macrodef".equals(qName) && configurator.getDepth() == 1) {
                  configurator.endMacroDef();
                  currentConfiguratorTag = null;
              } else {
                  configurator.endCreateChild();
              }
          }
      }
  
      @Override
      public void endDocument() throws SAXException {
          if (defaultResolver != null) {
              ivy.setDefaultResolver(ivy.substitute(defaultResolver));
          }
          if (defaultCM != null) {
              ConflictManager conflictManager = ivy.getConflictManager(ivy.substitute(defaultCM));
              if (conflictManager == null) {
                  throw new IllegalArgumentException("unknown conflict manager "
                          + ivy.substitute(defaultCM));
              }
              ivy.setDefaultConflictManager(conflictManager);
          }
          if (defaultLatest != null) {
              LatestStrategy latestStrategy = ivy.getLatestStrategy(ivy.substitute(defaultLatest));
              if (latestStrategy == null) {
                  throw new IllegalArgumentException("unknown latest strategy "
                          + ivy.substitute(defaultLatest));
              }
              ivy.setDefaultLatestStrategy(latestStrategy);
          }
          if (defaultCacheManager != null) {
              RepositoryCacheManager cache = ivy.getRepositoryCacheManager(ivy
                      .substitute(defaultCacheManager));
              if (cache == null) {
                  throw new IllegalArgumentException("unknown cache manager "
                          + ivy.substitute(defaultCacheManager));
              }
              ivy.setDefaultRepositoryCacheManager(cache);
          }
          if (defaultCircular != null) {
              CircularDependencyStrategy strategy = ivy.getCircularDependencyStrategy(ivy
                      .substitute(defaultCircular));
              if (strategy == null) {
                  throw new IllegalArgumentException("unknown circular dependency strategy "
                          + ivy.substitute(defaultCircular));
              }
              ivy.setCircularDependencyStrategy(strategy);
          }
          if (defaultLock != null) {
              LockStrategy strategy = ivy.getLockStrategy(ivy.substitute(defaultLock));
              if (strategy == null) {
                  throw new IllegalArgumentException("unknown lock strategy "
                          + ivy.substitute(defaultLock));
              }
              ivy.setDefaultLockStrategy(strategy);
          }
      }
  }
  