// Copyright 2010 Google Inc.
//
// Licensed 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 com.google.enterprise.connector.ldap;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Multimap;
import com.google.enterprise.connector.spi.SpiConstants;

import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Uses an an LdapHandler to implement JsonDocumentFetcher.
 */
public class LdapJsonDocumentFetcher implements JsonDocumentFetcher {

  private static final Logger LOG = Logger.getLogger(LdapJsonDocumentFetcher.class.getName());
  
  // It's okay for multiple connector instances to use this same counter,
  // it only affects the duration of wait
  private static volatile int waitcounter = 0; 
  
  /** default wait times are 1, 2, 4, 8 and 15 minutes, assigned in milliseconds.
   */
  private final int[] waitTimes; 
  
  private final Supplier<Map<String, Multimap<String, String>>> mapOfMultimapsSupplier;


  /**
   * Creates a JsonDocument fetcher from something that provides a sorted map of
   * Multimaps
   *
   * @param mapOfMultimapsSupplier An object that can supply a sorted Map of
   *        Multimaps. This supplier must permit multiple calls to get() on the
   *        same instance. Calls may return different results. The map must be
   *        in natural key order, that is, the order of the entrySet() must be
   *        ascending by key. The values (the individual multimaps) represent a
   *        document as a bag of (metadata-name,value) pairs.
   */
  public LdapJsonDocumentFetcher(
      Supplier<Map<String, Multimap<String, String>>> mapOfMultimapsSupplier) {
    this.mapOfMultimapsSupplier = mapOfMultimapsSupplier;
    this.waitTimes = new int[] { 1 * 60 * 1000, 2 * 60 * 1000, 4 * 60 * 1000, 
    		  8 * 60 * 1000, 15 * 60 * 1000 }; 
  }

  public LdapJsonDocumentFetcher(
          Supplier<Map<String, Multimap<String, String>>> mapOfMultimapsSupplier, 
          int [] waitTimes) {
        this.mapOfMultimapsSupplier = mapOfMultimapsSupplier;
        this.waitTimes = waitTimes;
  }
  
  private static Function<Entry<String, Multimap<String, String>>, Multimap<String, String>> addDocid =
      new Function<Entry<String, Multimap<String, String>>, Multimap<String, String>>() {
    @Override
    public Multimap<String, String> apply(Entry<String, Multimap<String, String>> e) {
      Multimap<String, String> person = ArrayListMultimap.create(e.getValue());
      String key = e.getKey();
      key = cleanLdapKey(key);
      person.put(SpiConstants.PROPNAME_DOCID, key);
      person.put(SpiConstants.PROPNAME_LOCK, "true");
      return person;
    }
  };

  @Override
  public Iterator<JsonDocument> iterator() {
    Map<String, Multimap<String, String>> results;

    try {
      results = mapOfMultimapsSupplier.get();
      // reset wait counter
      waitcounter = 0;
    } catch (LdapTransientException e) {
      LOG.log(Level.SEVERE, "Encountered IllegalStateException, will wait and continue.", e);
      results = Collections.emptyMap();
      try {
        // wait for some time to check if the unavailable ldap source would be
        // back
        long sleepTime;
        if (waitcounter < waitTimes.length) {
          sleepTime = waitTimes[waitcounter];
        } else {
          sleepTime = waitTimes[waitTimes.length - 1];
        }
        LOG.info("Waiting for " + sleepTime + " milliseconds.");
        Thread.sleep(sleepTime);
        waitcounter++;
      } catch (InterruptedException e1) {
        LOG.info("InterruptedException at CommunicationException catch..");
      }
    }

    return Iterators.transform(results.entrySet().iterator(),
        Functions.compose(JsonDocument.buildFromMultimap, addDocid));
  }

  /**
   * Creates a URL-safe encoding of the key that preserves collation
   * sequence. Since the keys are already in sorted order, we need to
   * encode them in a way that preserves order.
   *
   * We do this by encoding to UTF-8, then as hex of the byte sequence. The
   * reasons why this preserves order are complicated. See
   * http://unicode.org/reports/tr26/ for more, and note that this is tested by
   * I18NLdapJsonDocumentFetcherTest and other tests that extend
   * JsonDocumentFetcherTestCase.
   **/
  public static String cleanLdapKey(String key) {
    byte[] byteArray = key.getBytes(Charsets.UTF_8);
    StringBuffer sb = new StringBuffer(byteArray.length * 2);
    for (byte b : byteArray) {
      sb.append(String.format("%02x", b));
    }
    return sb.toString();
  }
}

@@ -654,21 +654,37 @@
 mon.
-base.Charsets
+annotations.VisibleForTesting
 ;%0Aim
@@ -997,16 +997,68 @@
 nstants;
+%0Aimport com.google.enterprise.connector.util.Base16;
 %0A%0Aimport
@@ -4722,24 +4722,31 @@
 he key that 
+mostly 
 preserves co
@@ -4746,356 +4746,515 @@
 rves
- collation%0A   * sequence. Since the keys are already in
+%0A   * binary order.%0A   * We do this by encoding to UTF-8, then hex encoding the byte sequence.%0A   * This method does not preserve the
  sort
-ed
  order
-, we need to%0A   * encode them in a way that
+ of the%0A   * input strings in the
  prese
-rves order.%0A   *%0A   * We do this by encoding to UTF-8, then as hex of the byte sequence. The%0A   * reasons why this preserves order are complicated. See%0A   * http://unicode.org/reports/tr26/ for more, and note that this is tested by
+nce of Unicode supplementary%0A   * characters.%0A   **/%0A  /*%0A   * TODO(jlacey): The original assumption here (that Java's UTF-8 is%0A   * really CESU-8) was wrong. This code only preserves order if the%0A   * original order was compatible with Unicode code points, or no%0A   * supplementary characters are used (which is the case in
 %0A   
@@ -5351,25 +5351,39 @@
 Case
+)
 .%0A   
-*
 */%0A  
-public
+@VisibleForTesting%0A 
  sta
@@ -5428,222 +5428,44 @@
     
-byte%5B%5D byteArray = key.getBytes(Charsets.UTF_8);%0A    StringBuffer sb = new StringBuffer(byteArray.length * 2);%0A    for (byte b : byteArray) %7B%0A      sb.append(String.format(%22%2502x%22, b));%0A    %7D%0A    return sb.toString(
+return Base16.lowerCase().encode(key
 );%0A 

Use new CM Base16 class for hex string encoding.