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.ambari.server.stack;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.state.ExtensionInfo;
  import org.apache.ambari.server.state.ServiceInfo;
  import org.apache.ambari.server.state.stack.ExtensionMetainfoXml;
  import org.apache.ambari.server.state.stack.ServiceMetainfoXml;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Extension module which provides all functionality related to parsing and fully
   * resolving extensions from the extension definition.
   *
   * An extension version is like a stack version but it contains custom services.  Linking an extension
   * version to the current stack version allows the cluster to install the custom services contained in
   * the extension version.
   *
   * <p>
   * Each extension node is identified by name and version, contains service
   * child nodes and may extend a single parent extension.
   * </p>
   *
   * <p>
   * Resolution of a extension is a depth first traversal up the inheritance chain where each extension node
   * calls resolve on its parent before resolving itself.  After the parent resolve call returns, all
   * ancestors in the inheritance tree are fully resolved.  The act of resolving the extension includes
   * resolution of the services children of the extension as well as merging of other extension
   * state with the fully resolved parent.
   * </p>
   *
   * <p>
   * Because a service may explicitly extend another service in a extension outside of the inheritance tree,
   * service child node resolution involves a depth first resolution of the extension associated with the
   * services explicit parent, if any.  This follows the same steps defined above fore extension node
   * resolution.  After the services explicit parent is fully resolved, the services state is merged
   * with it's parent.
   * </p>
   *
   * <p>
   * If a cycle in a extension definition is detected, an exception is thrown from the resolve call.
   * </p>
   *
   */
  public class ExtensionModule extends BaseModule<ExtensionModule, ExtensionInfo> implements Validable {
  
    /**
     * Context which provides access to external functionality
     */
    private StackContext stackContext;
  
    /**
     * Map of child configuration modules keyed by configuration type
     */
    private Map<String, ConfigurationModule> configurationModules = new HashMap<>();
  
    /**
     * Map of child service modules keyed by service name
     */
    private Map<String, ServiceModule> serviceModules = new HashMap<>();
  
    /**
     * Corresponding ExtensionInfo instance
     */
    private ExtensionInfo extensionInfo;
  
    /**
     * Encapsulates IO operations on extension directory
     */
    private ExtensionDirectory extensionDirectory;
  
    /**
     * Extension id which is in the form extensionName:extensionVersion
     */
    private String id;
  
    /**
     * validity flag
     */
    protected boolean valid = true;
  
    /**
     * Logger
     */
    private final static Logger LOG = LoggerFactory.getLogger(ExtensionModule.class);
  
    /**
     * Constructor.
     * @param extensionDirectory  represents extension directory
     * @param stackContext    general stack context
     */
    public ExtensionModule(ExtensionDirectory extensionDirectory, StackContext stackContext) {
      this.extensionDirectory = extensionDirectory;
      this.stackContext = stackContext;
      this.extensionInfo = new ExtensionInfo();
      populateExtensionInfo();
    }
  
    public Map<String, ServiceModule> getServiceModules() {
  	  return serviceModules;
    }
  
    /**
     * Fully resolve the extension. See extension resolution description in the class documentation.
     * If the extension has a parent, this extension will be merged against its fully resolved parent
     * if one is specified. Merging applies to all extension state including child service and
     * configuration modules.  Services may extend a service in another version in the
     * same extension hierarchy or may explicitly extend a service in a different
     * hierarchy.
     *
     * @param parentModule   not used.  Each extension determines its own parent since extensions don't
     *                       have containing modules
     * @param allStacks      all stacks modules contained in the stack definition
     * @param commonServices all common services
     * @param extensions     all extensions
     *
     * @throws AmbariException if an exception occurs during extension resolution
     */
    @Override
    public void resolve(
        ExtensionModule parentModule, Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions)
        throws AmbariException {
      moduleState = ModuleState.VISITED;
      checkExtensionName(allStacks);
  
      String parentVersion = extensionInfo.getParentExtensionVersion();
      mergeServicesWithExplicitParent(allStacks, commonServices, extensions);
      // merge with parent version of same extension definition
      if (parentVersion != null) {
        mergeExtensionWithParent(parentVersion, allStacks, commonServices, extensions);
      }
      moduleState = ModuleState.RESOLVED;
    }
  
    @Override
    public ExtensionInfo getModuleInfo() {
      return extensionInfo;
    }
  
    @Override
    public boolean isDeleted() {
      return false;
    }
  
    @Override
    public String getId() {
      return id;
    }
  
    @Override
    public void finalizeModule() {
      finalizeChildModules(serviceModules.values());
      finalizeChildModules(configurationModules.values());
    }
  
    /**
     * Get the associated extension directory.
     *
     * @return associated extension directory
     */
    public ExtensionDirectory getExtensionDirectory() {
      return extensionDirectory;
    }
  
    /**
     * Merge the extension with its parent.
     *
     * @param allStacks      all stacks in stack definition
     * @param commonServices all common services specified in the stack definition
     * @param parentVersion  version of the extensions parent
     *
     * @throws AmbariException if an exception occurs merging with the parent
     */
    private void mergeExtensionWithParent(
        String parentVersion, Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions)
        throws AmbariException {
  
      String parentExtensionKey = extensionInfo.getName() + StackManager.PATH_DELIMITER + parentVersion;
      ExtensionModule parentExtension = extensions.get(parentExtensionKey);
  
      if (parentExtension == null) {
        throw new AmbariException("Extension '" + extensionInfo.getName() + ":" + extensionInfo.getVersion() +
            "' specifies a parent that doesn't exist");
      }
  
      resolveExtension(parentExtension, allStacks, commonServices, extensions);
      /*mergeConfigurations(parentStack, allStacks, commonServices);
      mergeRoleCommandOrder(parentStack);*/
  
      /*if (extensionInfo.getStackHooksFolder() == null) {
        extensionInfo.setStackHooksFolder(parentStack.getModuleInfo().getStackHooksFolder());
      }
  
      if (extensionInfo.getKerberosDescriptorFileLocation() == null) {
        extensionInfo.setKerberosDescriptorFileLocation(parentStack.getModuleInfo().getKerberosDescriptorFileLocation());
      }
  
      if (extensionInfo.getWidgetsDescriptorFileLocation() == null) {
        extensionInfo.setWidgetsDescriptorFileLocation(parentStack.getModuleInfo().getWidgetsDescriptorFileLocation());
      }*/
  
      mergeServicesWithParent(parentExtension, allStacks, commonServices, extensions);
    }
  
    /**
     * Merge child services with parent extension.
     *
     * @param parentExtension    parent extension module
     * @param allStacks          all stacks in stack definition
     * @param commonServices     all common services
     * @param extensions         all extensions
     *
     * @throws AmbariException if an exception occurs merging the child services with the parent extension
     */
    private void mergeServicesWithParent(
        ExtensionModule parentExtension, Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions)
        throws AmbariException {
      extensionInfo.getServices().clear();
  
      LOG.info("***Merging extension services with parent: " + parentExtension.getId());
  
      Collection<ServiceModule> mergedModules = mergeChildModules(
          allStacks, commonServices, extensions, serviceModules, parentExtension.serviceModules);
      for (ServiceModule module : mergedModules) {
        if(!module.isDeleted()){
          serviceModules.put(module.getId(), module);
          extensionInfo.getServices().add(module.getModuleInfo());
        }
      }
    }
  
    /**
     * Merge services with their explicitly specified parent if one has been specified.
     * @param allStacks      all stacks in stack definition
     * @param commonServices all common services specified in the stack definition
     *
     * @throws AmbariException if an exception occurs while merging child services with their explicit parents
     */
    private void mergeServicesWithExplicitParent(
          Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions) throws AmbariException {
      for (ServiceModule service : serviceModules.values()) {
        ServiceInfo serviceInfo = service.getModuleInfo();
        String parent = serviceInfo.getParent();
        if (parent != null) {
          mergeServiceWithExplicitParent(service, parent, allStacks, commonServices, extensions);
        }
      }
    }
  
    /**
     * Merge a service with its explicitly specified parent.
     * @param service          the service to merge
     * @param parent           the explicitly specified parent service
     * @param allStacks        all stacks specified in the stack definition
     * @param commonServices   all common services specified in the stack definition
     *
     * @throws AmbariException if an exception occurs merging a service with its explicit parent
     */
    private void mergeServiceWithExplicitParent(
        ServiceModule service, String parent, Map<String, StackModule> allStacks,
        Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions)
        throws AmbariException {
      if(isCommonServiceParent(parent)) {
        LOG.info("merging with common service: " + service.getModuleInfo().getName());
        mergeServiceWithCommonServiceParent(service, parent, allStacks, commonServices, extensions);
        LOG.info("display name: " + service.getModuleInfo().getDisplayName());
      } else {
        throw new AmbariException("The service '" + service.getModuleInfo().getName() + "' in extension '" + extensionInfo.getName() + ":"
            + extensionInfo.getVersion() + "' extends an invalid parent: '" + parent + "'");
      }
    }
  
    /**
     * @param allStacks        all stacks specified in the stack definition
     *
     * @throws AmbariException if the extension name is the same as any of the stacks
     */
    private void checkExtensionName(Map<String, StackModule> allStacks)
        throws AmbariException {
  
      String name = extensionInfo.getName();
      for (StackModule stack : allStacks.values()) {
        String stackName = stack.getModuleInfo().getName();
        if (name.equals(stackName)) {
          throw new AmbariException("The extension '" + name + "' has a name which matches a stack name");
        }
      }
    }
  
    /**
     * Check if parent is common service
     * @param parent  Parent string
     * @return true: if parent is common service, false otherwise
     */
    private boolean isCommonServiceParent(String parent) {
      return parent != null
          && !parent.isEmpty()
          && parent.split(StackManager.PATH_DELIMITER)[0].equalsIgnoreCase(StackManager.COMMON_SERVICES);
    }
  
    /**
     * Merge a service with its explicitly specified common service as parent.
     * Parent: common-services/<serviceName>/<serviceVersion>
     * Common Services Lookup Key: <serviceName>/<serviceVersion>
     * Example:
     *  Parent: common-services/HDFS/2.1.0.2.0
     *  Key: HDFS/2.1.0.2.0
     *
     * @param service          the service to merge
     * @param parent           the explicitly specified common service as parent
     * @param allStacks        all stacks specified in the stack definition
     * @param commonServices   all common services specified in the stack definition
     * @throws AmbariException
     */
    private void mergeServiceWithCommonServiceParent(
        ServiceModule service, String parent, Map<String, StackModule> allStacks,
        Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions)
        throws AmbariException {
      ServiceInfo serviceInfo = service.getModuleInfo();
      String[] parentToks = parent.split(StackManager.PATH_DELIMITER);
      if(parentToks.length != 3 || !parentToks[0].equalsIgnoreCase(StackManager.COMMON_SERVICES)) {
        throw new AmbariException("The service '" + serviceInfo.getName() + "' in extension '" + extensionInfo.getName() + ":"
            + extensionInfo.getVersion() + "' extends an invalid parent: '" + parent + "'");
      }
  
      String baseServiceKey = parentToks[1] + StackManager.PATH_DELIMITER + parentToks[2];
      ServiceModule baseService = commonServices.get(baseServiceKey);
      if (baseService == null) {
        setValid(false);
        extensionInfo.setValid(false);
        String error = "The service '" + serviceInfo.getName() + "' in extension '" + extensionInfo.getName() + ":"
            + extensionInfo.getVersion() + "' extends a non-existent service: '" + parent + "'";
        addError(error);
        extensionInfo.addError(error);
      } else {
        if (baseService.isValid()) {
          service.resolveExplicit(baseService, allStacks, commonServices, extensions);
        } else {
          setValid(false);
          extensionInfo.setValid(false);
          addErrors(baseService.getErrors());
          extensionInfo.addErrors(baseService.getErrors());
        }
      }
    }
  
    /**
     * Populate the extension module and info from the extension definition.
     */
    private void populateExtensionInfo() {
      extensionInfo.setName(extensionDirectory.getExtensionDirName());
      extensionInfo.setVersion(extensionDirectory.getName());
  
      id = String.format("%s:%s", extensionInfo.getName(), extensionInfo.getVersion());
  
      LOG.debug("Adding new extension to known extensions, extensionName = {}, extensionVersion = {}", extensionInfo.getName(), extensionInfo.getVersion());
  
  
      //todo: give additional thought on handling missing metainfo.xml
      ExtensionMetainfoXml emx = extensionDirectory.getMetaInfoFile();
      if (emx != null) {
        if (!emx.isValid()) {
          extensionInfo.setValid(false);
          extensionInfo.addErrors(emx.getErrors());
        }
        extensionInfo.setParentExtensionVersion(emx.getExtends());
        extensionInfo.setStacks(emx.getStacks());
        extensionInfo.setExtensions(emx.getExtensions());
        extensionInfo.setActive(emx.getVersion().isActive());
        extensionInfo.setAutoLink(emx.isAutoLink());
      }
  
      try {
        // Read the service for this extension
        populateServices();
        if (!extensionInfo.isValid()) {
          setValid(false);
          addErrors(extensionInfo.getErrors());
        }
  
        //todo: shouldn't blindly catch Exception, re-evaluate this.
      } catch (Exception e) {
        String error = "Exception caught while populating services for extension: " +
            extensionInfo.getName() + "-" + extensionInfo.getVersion();
        setValid(false);
        extensionInfo.setValid(false);
        addError(error);
        extensionInfo.addError(error);
        LOG.error(error);
      }
    }
  
    /**
     * Populate the child services.
     */
    private void populateServices()throws AmbariException {
      for (ServiceDirectory serviceDir : extensionDirectory.getServiceDirectories()) {
        populateService(serviceDir);
      }
    }
  
    /**
     * Populate a child service.
     *
     * @param serviceDirectory the child service directory
     */
    private void populateService(ServiceDirectory serviceDirectory)  {
      Collection<ServiceModule> serviceModules = new ArrayList<>();
      // unfortunately, we allow multiple services to be specified in the same metainfo.xml,
      // so we can't move the unmarshal logic into ServiceModule
      ServiceMetainfoXml metaInfoXml = serviceDirectory.getMetaInfoFile();
      if (!metaInfoXml.isValid()){
        extensionInfo.setValid(metaInfoXml.isValid());
        setValid(metaInfoXml.isValid());
        extensionInfo.addErrors(metaInfoXml.getErrors());
        addErrors(metaInfoXml.getErrors());
        return;
      }
      List<ServiceInfo> serviceInfos = metaInfoXml.getServices();
  
      for (ServiceInfo serviceInfo : serviceInfos) {
        ServiceModule serviceModule = new ServiceModule(stackContext, serviceInfo, serviceDirectory);
        serviceModules.add(serviceModule);
        if (!serviceModule.isValid()){
          extensionInfo.setValid(false);
          setValid(false);
          extensionInfo.addErrors(serviceModule.getErrors());
          addErrors(serviceModule.getErrors());
        }
      }
      addServices(serviceModules);
    }
  
    /**
     * Resolve another extension module.
     *
     * @param parentExtension    extension module to be resolved
     * @param allStacks          all stack modules in stack definition
     * @param commonServices     all common services specified in the stack definition
     * @param extensions         all extensions
     * @throws AmbariException if unable to resolve the extension
     */
    private void resolveExtension(
            ExtensionModule parentExtension, Map<String, StackModule> allStacks, Map<String, ServiceModule> commonServices, Map<String, ExtensionModule> extensions)
            throws AmbariException {
      if (parentExtension.getModuleState() == ModuleState.INIT) {
  	  parentExtension.resolve(null, allStacks, commonServices, extensions);
      } else if (parentExtension.getModuleState() == ModuleState.VISITED) {
        //todo: provide more information to user about cycle
        throw new AmbariException("Cycle detected while parsing extension definition");
      }
      if (!parentExtension.isValid() || (parentExtension.getModuleInfo() != null && !parentExtension.getModuleInfo().isValid())) {
        setValid(parentExtension.isValid());
        extensionInfo.setValid(parentExtension.extensionInfo.isValid());
        addErrors(parentExtension.getErrors());
        extensionInfo.addErrors(parentExtension.getErrors());
      }
    }
  
    /**
     * Add a child service module to the extension.
     *
     * @param service  service module to add
     */
    private void addService(ServiceModule service) {
      ServiceInfo serviceInfo = service.getModuleInfo();
      Object previousValue = serviceModules.put(service.getId(), service);
      if (previousValue == null) {
        extensionInfo.getServices().add(serviceInfo);
      }
    }
  
    /**
     * Add child service modules to the extension.
     *
     * @param services  collection of service modules to add
     */
    private void addServices(Collection<ServiceModule> services) {
      for (ServiceModule service : services) {
        addService(service);
      }
    }
  
    @Override
    public boolean isValid() {
      return valid;
    }
  
    @Override
    public void setValid(boolean valid) {
      this.valid = valid;
    }
  
    private Set<String> errorSet = new HashSet<>();
  
    @Override
    public Collection<String> getErrors() {
      return errorSet;
    }
  
    @Override
    public void addError(String error) {
      errorSet.add(error);
    }
  
    @Override
    public void addErrors(Collection<String> errors) {
      this.errorSet.addAll(errors);
    }
  
  }
  