/*
 * 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.hive.service.auth.ldap;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchResult;
import org.apache.hadoop.hive.conf.HiveConf;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implements search for LDAP.
 */
public final class LdapSearch implements DirSearch {

  private static final Logger LOG = LoggerFactory.getLogger(LdapSearch.class);

  private final String baseDn;
  private final List<String> groupBases;
  private final List<String> userBases;
  private final List<String> userPatterns;

  private final QueryFactory queries;

  private final DirContext ctx;

  /**
   * Construct an instance of {@code LdapSearch}.
   * @param conf Hive configuration
   * @param ctx Directory service that will be used for the queries.
   * @throws NamingException
   */
  public LdapSearch(HiveConf conf, DirContext ctx) throws NamingException {
    baseDn = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN);
    userPatterns = LdapUtils.parseDnPatterns(conf,
        HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN);
    groupBases = LdapUtils.patternsToBaseDns(LdapUtils.parseDnPatterns(conf,
        HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN));
    userBases = LdapUtils.patternsToBaseDns(userPatterns);
    this.ctx = ctx;
    queries = new QueryFactory(conf);
  }

  /**
   * Closes this search object and releases any system resources associated
   * with it. If the search object is already closed then invoking this
   * method has no effect.
   */
  @Override
  public void close() {
    try {
      ctx.close();
    } catch (NamingException e) {
      LOG.warn("Exception when closing LDAP context:", e);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String findUserDn(String user) throws NamingException {
    List<String> allLdapNames;
    if (LdapUtils.isDn(user)) {
      String userBaseDn = LdapUtils.extractBaseDn(user);
      String userRdn = LdapUtils.extractFirstRdn(user);
      allLdapNames = execute(Collections.singletonList(userBaseDn),
          queries.findUserDnByRdn(userRdn)).getAllLdapNames();
    } else {
      allLdapNames = findDnByPattern(userPatterns, user);
      if (allLdapNames.isEmpty()) {
        allLdapNames = execute(userBases, queries.findUserDnByName(user)).getAllLdapNames();
      }
    }

    if (allLdapNames.size() == 1) {
      return allLdapNames.get(0);
    } else {
      LOG.info("Expected exactly one user result for the user: {}, but got {}. Returning null",
          user, allLdapNames.size());
      LOG.debug("Matched users: {}", allLdapNames);
      return null;
    }
  }

  private List<String> findDnByPattern(List<String> patterns, String name) throws NamingException {
    for (String pattern : patterns) {
      String baseDnFromPattern = LdapUtils.extractBaseDn(pattern);
      String rdn = LdapUtils.extractFirstRdn(pattern).replaceAll("%s", name);
      List<String> list = execute(Collections.singletonList(baseDnFromPattern),
          queries.findDnByPattern(rdn)).getAllLdapNames();
      if (!list.isEmpty()) {
        return list;
      }
    }
    return Collections.emptyList();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String findGroupDn(String group) throws NamingException {
    return execute(groupBases, queries.findGroupDnById(group)).getSingleLdapName();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isUserMemberOfGroup(String user, String groupDn) throws NamingException {
    String userId = LdapUtils.extractUserName(user);
    return execute(userBases, queries.isUserMemberOfGroup(userId, groupDn)).hasSingleResult();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List<String> findGroupsForUser(String userDn) throws NamingException {
    String userName = LdapUtils.extractUserName(userDn);
    return execute(groupBases, queries.findGroupsForUser(userName, userDn)).getAllLdapNames();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public List<String> executeCustomQuery(String query) throws NamingException {
    return execute(Collections.singletonList(baseDn), queries.customQuery(query))
        .getAllLdapNamesAndAttributes();
  }

  private SearchResultHandler execute(Collection<String> baseDns, Query query) {
    List<NamingEnumeration<SearchResult>> searchResults = new ArrayList<>();
    LOG.debug("Executing a query: '{}' with base DNs {}.", query.getFilter(), baseDns);
    for (String aBaseDn : baseDns) {
      try {
        NamingEnumeration<SearchResult> searchResult = ctx.search(aBaseDn, query.getFilter(),
            query.getControls());
        if (searchResult != null) {
          searchResults.add(searchResult);
        }
      } catch (NamingException ex) {
        LOG.debug("Exception happened for query '" + query.getFilter() +
            "' with base DN '" + aBaseDn + "'", ex);
      }
    }
    return new SearchResultHandler(searchResults);
  }
}
