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.io.File;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  
  import javax.annotation.Nullable;
  
  import org.apache.ambari.annotations.Experimental;
  import org.apache.ambari.annotations.ExperimentalFeature;
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.controller.RepositoryResponse;
  import org.apache.ambari.server.orm.entities.RepoDefinitionEntity;
  import org.apache.ambari.server.orm.entities.RepoOsEntity;
  import org.apache.ambari.server.state.RepositoryInfo;
  import org.apache.ambari.server.state.stack.RepositoryXml;
  import org.apache.commons.collections4.CollectionUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.common.base.Function;
  import com.google.common.base.Optional;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Iterables;
  import com.google.common.collect.ListMultimap;
  import com.google.common.collect.Lists;
  import com.google.common.collect.Multimaps;
  import com.google.common.collect.Sets;
  
  /**
   * Utility functions for repository related tasks.
   */
  public class RepoUtil {
  
    /**
     * logger instance
     */
    private final static Logger LOG = LoggerFactory.getLogger(RepoUtil.class);
  
  
    /**
     * repository directory name
     */
    final static String REPOSITORY_FOLDER_NAME = "repos";
  
    /**
     * repository file name
     */
    final static String REPOSITORY_FILE_NAME = "repoinfo.xml";
  
    private static final Function<RepoDefinitionEntity, String> REPO_ENTITY_TO_NAME = new Function<RepoDefinitionEntity, String>() {
      @Override
      public String apply(RepoDefinitionEntity input) {
        return input.getRepoName();
      }
    };
  
  
    /**
     * Parses the repository file for a stack/service if exists.
     *
     * @param directory stack/service base directory
     * @param subDirs stack/service directory sub directories
     * @param unmarshaller {@link ModuleFileUnmarshaller}, needed to parse repo XML
     * @throws AmbariException if unable to parse the repository file
     * @return The directory containing the repo file and the parsed repo file (if exists)
     */
    public static RepositoryFolderAndXml parseRepoFile(File directory,
                                                   Collection<String> subDirs,
                                                   ModuleFileUnmarshaller unmarshaller) {
      File repositoryFile = null;
      String repoDir = null;
      RepositoryXml repoFile = null;
  
      if (subDirs.contains(REPOSITORY_FOLDER_NAME)) {
        repoDir = directory.getAbsolutePath() + File.separator + REPOSITORY_FOLDER_NAME;
        repositoryFile = new File(directory.getPath()+ File.separator +
            REPOSITORY_FOLDER_NAME + File.separator + REPOSITORY_FILE_NAME);
  
        if (repositoryFile.exists()) {
          try {
            repoFile = unmarshaller.unmarshal(RepositoryXml.class, repositoryFile);
          } catch (Exception e) {
            repoFile = new RepositoryXml();
            repoFile.setValid(false);
            String msg = "Unable to parse repo file at location: " +
                repositoryFile.getAbsolutePath();
            repoFile.addError(msg);
            LOG.warn(msg);
          }
        }
      }
  
      return new RepositoryFolderAndXml(Optional.fromNullable(repoDir), Optional.fromNullable(repoFile));
    }
  
    /**
     * Checks the passed {@code operatingSystems} parameter if it contains all repositories from the stack model. If a
     *    repository is present in the stack model but missing in the operating system entity list, it is considered a
     *    service repository and will be added.
     * @param operatingSystems - A list of OperatingSystemEntity objects extracted from a RepositoryVersionEntity
     * @param stackReposByOs - Stack repositories loaded from the disk (including service repositories), grouped by os.
     * @return {@code true} if there were added repositories
     */
    @Experimental(feature = ExperimentalFeature.CUSTOM_SERVICE_REPOS,
      comment = "Remove logic for handling custom service repos after enabling multi-mpack cluster deployment")
    public static boolean addServiceReposToOperatingSystemEntities(List<RepoOsEntity> operatingSystems,
                                                                   ListMultimap<String, RepositoryInfo> stackReposByOs) {
      Set<String> addedRepos = new HashSet<>();
      for (RepoOsEntity os : operatingSystems) {
        List<RepositoryInfo> serviceReposForOs = stackReposByOs.get(os.getFamily());
        ImmutableSet<String> repoNames = ImmutableSet.copyOf(Lists.transform(os.getRepoDefinitionEntities(), REPO_ENTITY_TO_NAME));
        for (RepositoryInfo repoInfo : serviceReposForOs)
          if (!repoNames.contains(repoInfo.getRepoName())) {
            os.addRepoDefinition(toRepositoryEntity(repoInfo));
            addedRepos.add(String.format("%s (%s)", repoInfo.getRepoId(), os.getFamily()));
          }
      }
      LOG.info("Added {} service repos: {}", addedRepos.size(),Iterables.toString(addedRepos));
  
      return CollectionUtils.isNotEmpty(addedRepos);
    }
  
    /**
     * Given a list of VDF repositorie and stack repositories (grouped by os) returns the service repositories.
     * A repository is considered a service repo if present in the stack model but missing in the VDF (check is performed
     * by repository name, per operating system).
     * @param vdfRepos the repositories coming from a version definition
     * @param stackReposByOs the repositories in the stack model (loaded from disks)
     * @return A list of service repositories
     */
    @Experimental(feature = ExperimentalFeature.CUSTOM_SERVICE_REPOS,
      comment = "Remove logic for handling custom service repos after enabling multi-mpack cluster deployment")
    public static List<RepositoryInfo> getServiceRepos(List<RepositoryInfo> vdfRepos,
                                                     ListMultimap<String, RepositoryInfo> stackReposByOs) {
      Set<String> serviceRepoIds = new HashSet<>();
      List<RepositoryInfo> serviceRepos = new ArrayList<>();
      ListMultimap<String, RepositoryInfo> vdfReposByOs = Multimaps.index(vdfRepos, RepositoryInfo.GET_OSTYPE_FUNCTION);
      for(String os: vdfReposByOs.keySet()) {
        Set<String> vdfRepoNames = Sets.newHashSet(
            Lists.transform(vdfReposByOs.get(os), RepositoryInfo.GET_REPO_NAME_FUNCTION));
        for (RepositoryInfo repo: stackReposByOs.get(os)) {
          if (!vdfRepoNames.contains(repo.getRepoName())) {
            serviceRepos.add(repo);
            serviceRepoIds.add(repo.getRepoId());
          }
        }
      }
      LOG.debug("Found {} service repos: {}", serviceRepoIds.size(),Iterables.toString(serviceRepoIds));
      return serviceRepos;
    }
  
    /**
     * Convert a list of {@link RepositoryInfo} objects to a lost of {@link RepositoryResponse} objects
     * @param repositoryInfos the list of repository infos
     * @param versionDefinitionId the version definition id
     * @param stackName the stack name
     * @param stackVersion the stack version
     * @return a list of repository responses
     */
    public static List<RepositoryResponse> asResponses(List<RepositoryInfo> repositoryInfos,
                                                       @Nullable String versionDefinitionId,
                                                       @Nullable String stackName,
                                                       @Nullable String stackVersion) {
      List<RepositoryResponse> responses = new ArrayList<>(repositoryInfos.size());
      for (RepositoryInfo repoInfo: repositoryInfos) {
        RepositoryResponse response = repoInfo.convertToResponse();
        response.setVersionDefinitionId(versionDefinitionId);
        response.setStackName(stackName);
        response.setStackVersion(stackVersion);
        responses.add(response);
      }
      return responses;
    }
  
    @Experimental(feature = ExperimentalFeature.CUSTOM_SERVICE_REPOS,
      comment = "Remove logic for handling custom service repos after enabling multi-mpack cluster deployment")
    private static RepoDefinitionEntity toRepositoryEntity(RepositoryInfo repoInfo) {
      RepoDefinitionEntity re = new RepoDefinitionEntity();
      re.setBaseUrl(repoInfo.getBaseUrl());
      re.setRepoName(repoInfo.getRepoName());
      re.setRepoID(repoInfo.getRepoId());
      re.setDistribution(repoInfo.getDistribution());
      re.setComponents(repoInfo.getComponents());
      re.setTags(repoInfo.getTags());
      re.setApplicableServices(repoInfo.getApplicableServices());
      return re;
    }
  
  }
  
  /**
   * Value class for a pair of repository folder and parsed repository XML.
   */
  class RepositoryFolderAndXml {
      final Optional<String> repoDir;
      final Optional<RepositoryXml> repoXml;
  
      /**
       * @param repoDir Path to the repository directory (optional)
       * @param repoXml Parsed repository XML (optional)
       */
      public RepositoryFolderAndXml(Optional<String> repoDir, Optional<RepositoryXml> repoXml) {
        this.repoDir = repoDir;
        this.repoXml = repoXml;
      }
  }
  