/*
  $Id: Ldif.java 1330 2010-05-23 22:10:53Z dfisher $

  Copyright (C) 2003-2010 Virginia Tech.
  All rights reserved.

  SEE LICENSE FOR MORE INFORMATION

  Author:  Middleware Services
  Email:   middleware@vt.edu
  Version: $Revision: 1330 $
  Updated: $Date: 2010-05-24 00:10:53 +0200 (Mon, 24 May 2010) $
*/
package edu.vt.middleware.ldap.ldif;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.Writer;
import java.net.URL;
import java.util.Iterator;
import javax.naming.NamingException;
import javax.naming.directory.SearchResult;
import edu.vt.middleware.ldap.LdapUtil;
import edu.vt.middleware.ldap.bean.LdapAttribute;
import edu.vt.middleware.ldap.bean.LdapBeanFactory;
import edu.vt.middleware.ldap.bean.LdapBeanProvider;
import edu.vt.middleware.ldap.bean.LdapEntry;
import edu.vt.middleware.ldap.bean.LdapResult;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <code>Ldif</code> contains functions for converting LDAP search result sets
 * into LDIF.
 *
 * @author  Middleware Services
 * @version  $Revision: 1330 $ $Date: 2010-05-24 00:10:53 +0200 (Mon, 24 May 2010) $
 */
public class Ldif implements Serializable
{

  /** ASCII decimal value of nul. */
  public static final int NUL_CHAR = 0;

  /** ASCII decimal value of line feed. */
  public static final int LF_CHAR = 10;

  /** ASCII decimal value of carriage return. */
  public static final int CR_CHAR = 13;

  /** ASCII decimal value of space. */
  public static final int SP_CHAR = 32;

  /** ASCII decimal value of colon. */
  public static final int COLON_CHAR = 58;

  /** ASCII decimal value of left arrow. */
  public static final int LA_CHAR = 60;

  /** ASCII decimal value of highest character. */
  public static final int MAX_ASCII_CHAR = 127;

  /** serial version uid. */
  private static final long serialVersionUID = -3763879179455001975L;

  /** Line separator. */
  private static final String LINE_SEPARATOR = System.getProperty(
    "line.separator");

  /** Log for this class. */
  protected final Log logger = LogFactory.getLog(this.getClass());

  /** Ldap bean factory. */
  protected LdapBeanFactory beanFactory = LdapBeanProvider.getLdapBeanFactory();


  /**
   * Returns the factory for creating ldap beans.
   *
   * @return  <code>LdapBeanFactory</code>
   */
  public LdapBeanFactory getLdapBeanFactory()
  {
    return this.beanFactory;
  }


  /**
   * Sets the factory for creating ldap beans.
   *
   * @param  lbf  <code>LdapBeanFactory</code>
   */
  public void setLdapBeanFactory(final LdapBeanFactory lbf)
  {
    if (lbf != null) {
      this.beanFactory = lbf;
    }
  }


  /**
   * This will take the results of a prior LDAP query and convert it to LDIF.
   *
   * @param  results  <code>Iterator</code> of LDAP search results
   *
   * @return  <code>String</code>
   */
  public String createLdif(final Iterator<SearchResult> results)
  {
    String ldif = "";
    try {
      final LdapResult lr = this.beanFactory.newLdapResult();
      lr.addEntries(results);
      ldif = this.createLdif(lr);
    } catch (NamingException e) {
      if (this.logger.isErrorEnabled()) {
        this.logger.error("Error creating String from SearchResults", e);
      }
    }
    return ldif;
  }


  /**
   * This will take the results of a prior LDAP query and convert it to LDIF.
   *
   * @param  result  <code>LdapResult</code>
   *
   * @return  <code>String</code>
   */
  public String createLdif(final LdapResult result)
  {
    // build string from results
    final StringBuffer ldif = new StringBuffer();
    if (result != null) {
      for (LdapEntry le : result.getEntries()) {
        ldif.append(createLdifEntry(le));
      }
    }

    return ldif.toString();
  }


  /**
   * This will take an LDAP entry and convert it to LDIF.
   *
   * @param  ldapEntry  <code>LdapEntry</code> to convert
   *
   * @return  <code>String</code>
   */
  protected String createLdifEntry(final LdapEntry ldapEntry)
  {
    final StringBuffer entry = new StringBuffer();
    if (ldapEntry != null) {

      final String dn = ldapEntry.getDn();
      if (dn != null) {
        if (encodeData(dn)) {
          final String encodedDn = LdapUtil.base64Encode(dn);
          if (encodedDn != null) {
            entry.append("dn:: ").append(dn).append(LINE_SEPARATOR);
          }
        } else {
          entry.append("dn: ").append(dn).append(LINE_SEPARATOR);
        }
      }

      for (LdapAttribute attr : ldapEntry.getLdapAttributes().getAttributes()) {
        final String attrName = attr.getName();
        for (Object attrValue : attr.getValues()) {
          if (encodeData(attrValue)) {
            String encodedAttrValue = null;
            if (attrValue instanceof String) {
              encodedAttrValue = LdapUtil.base64Encode((String) attrValue);
            } else if (attrValue instanceof byte[]) {
              encodedAttrValue = LdapUtil.base64Encode((byte[]) attrValue);
            } else {
              if (this.logger.isWarnEnabled()) {
                this.logger.warn(
                  "Could not cast attribute value as a byte[]" +
                  " or a String");
              }
            }
            if (encodedAttrValue != null) {
              entry.append(attrName).append(":: ").append(encodedAttrValue)
                .append(LINE_SEPARATOR);
            }
          } else {
            entry.append(attrName).append(": ").append(attrValue).append(
              LINE_SEPARATOR);
          }
        }
      }
    }

    if (entry.length() > 0) {
      entry.append(LINE_SEPARATOR);
    }
    return entry.toString();
  }


  /**
   * This determines whether the supplied data should be base64 encoded. See
   * http://www.faqs.org/rfcs/rfc2849.html for more details.
   *
   * @param  data  <code>Object</code> to inspect
   *
   * @return  <code>boolean</code>
   */
  private boolean encodeData(final Object data)
  {
    boolean encode = false;
    if (data instanceof String) {
      final String stringData = (String) data;
      final char[] dataCharArray = stringData.toCharArray();
      for (int i = 0; i < dataCharArray.length; i++) {
        final int charInt = (int) dataCharArray[i];
        // check for NUL
        if (charInt == NUL_CHAR) {
          encode = true;
          // check for LF
        } else if (charInt == LF_CHAR) {
          encode = true;
          // check for CR
        } else if (charInt == CR_CHAR) {
          encode = true;
          // check for SP at beginning or end of string
        } else if (
          charInt == SP_CHAR &&
            (i == 0 || i == dataCharArray.length - 1)) {
          encode = true;
          // check for colon(:) at beginning of string
        } else if (charInt == COLON_CHAR && i == 0) {
          encode = true;
          // check for left arrow(<) at beginning of string
        } else if (charInt == LA_CHAR && i == 0) {
          encode = true;
          // check for any character above 127
        } else if (charInt > MAX_ASCII_CHAR) {
          encode = true;
        }
      }
    } else {
      encode = true;
    }
    return encode;
  }


  /**
   * This will write the supplied LDAP search results to the supplied writer in
   * LDIF form.
   *
   * @param  results  <code>Iterator</code> of LDAP search results
   * @param  writer  <code>Writer</code> to write to
   *
   * @throws  IOException  if an error occurs while writing to the output stream
   */
  public void outputLdif(
    final Iterator<SearchResult> results,
    final Writer writer)
    throws IOException
  {
    writer.write(createLdif(results));
    writer.flush();
  }


  /**
   * This will write the supplied LDAP search results to the supplied writer in
   * LDIF form.
   *
   * @param  result  <code>LdapResult</code>
   * @param  writer  <code>Writer</code> to write to
   *
   * @throws  IOException  if an error occurs while writing to the output stream
   */
  public void outputLdif(final LdapResult result, final Writer writer)
    throws IOException
  {
    writer.write(createLdif(result));
    writer.flush();
  }


  /**
   * This will take a Reader containing an LDIF and convert it to an Iterator of
   * LDAP search results. Provides a loose implementation of RFC 2849. Should
   * not be used to validate LDIF format as it does not enforce strictness.
   *
   * @param  reader  <code>Reader</code> containing LDIF content
   *
   * @return  <code>Iterator</code> - of LDAP search results
   *
   * @throws  IOException  if an I/O error occurs
   */
  public Iterator<SearchResult> importLdif(final Reader reader)
    throws IOException
  {
    return this.importLdifToLdapResult(reader).toSearchResults().iterator();
  }


  /**
   * This will take a Reader containing an LDIF and convert it to an <code>
   * LdapResult</code>. Provides a loose implementation of RFC 2849. Should not
   * be used to validate LDIF format as it does not enforce strictness.
   *
   * @param  reader  <code>Reader</code> containing LDIF content
   *
   * @return  <code>LdapResult</code> - LDAP search results
   *
   * @throws  IOException  if an I/O error occurs
   */
  public LdapResult importLdifToLdapResult(final Reader reader)
    throws IOException
  {
    final LdapResult ldapResult = this.beanFactory.newLdapResult();
    final BufferedReader br = new BufferedReader(reader);
    String line = null;
    int lineCount = 0;
    LdapEntry ldapEntry = null;
    StringBuffer lineValue = new StringBuffer();

    while ((line = br.readLine()) != null) {
      lineCount++;
      if (line.startsWith("dn:")) {
        lineValue.append(line);
        ldapEntry = this.beanFactory.newLdapEntry();
        break;
      }
    }

    boolean read = true;
    while (read) {
      line = br.readLine();
      if (line == null) {
        read = false;
        line = "";
      }
      if (!line.startsWith("#")) {
        if (line.startsWith("dn:")) {
          ldapResult.addEntry(ldapEntry);
          ldapEntry = this.beanFactory.newLdapEntry();
        }
        if (line.startsWith(" ")) {
          lineValue.append(line.substring(1));
        } else {
          final String s = lineValue.toString();
          if (s.indexOf(":") != -1) {
            boolean isBinary = false;
            boolean isUrl = false;
            final String[] parts = s.split(":", 2);
            final String attrName = parts[0];
            String attrValue = parts[1];
            if (attrValue.startsWith(":")) {
              isBinary = true;
              attrValue = attrValue.substring(1);
            } else if (attrValue.startsWith("<")) {
              isUrl = true;
              attrValue = attrValue.substring(1);
            }
            if (attrValue.startsWith(" ")) {
              attrValue = attrValue.substring(1);
            }
            if ("dn".equals(attrName)) {
              ldapEntry.setDn(attrValue);
            } else {
              LdapAttribute ldapAttr = ldapEntry.getLdapAttributes()
                  .getAttribute(attrName);
              if (ldapAttr == null) {
                ldapAttr = this.beanFactory.newLdapAttribute();
                ldapAttr.setName(attrName);
                ldapEntry.getLdapAttributes().addAttribute(ldapAttr);
              }
              if (isBinary) {
                ldapAttr.getValues().add(LdapUtil.base64Decode(attrValue));
              } else if (isUrl) {
                ldapAttr.getValues().add(LdapUtil.readURL(new URL(attrValue)));
              } else {
                ldapAttr.getValues().add(attrValue);
              }
            }
          }
          lineValue = new StringBuffer(line);
        }
      }
    }
    if (ldapEntry != null) {
      ldapResult.addEntry(ldapEntry);
    }
    return ldapResult;
  }
}
