/*
 * Copyright 2005-2010 the original author or authors.
 *
 * 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 org.springframework.ldap.ldif.parser;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.NoSuchElementException;

import javax.naming.NamingException;
import javax.naming.directory.Attribute;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapAttributes;
import org.springframework.ldap.ldif.InvalidRecordFormatException;
import org.springframework.ldap.ldif.support.AttributeValidationPolicy;
import org.springframework.ldap.ldif.support.DefaultAttributeValidationPolicy;
import org.springframework.ldap.ldif.support.LineIdentifier;
import org.springframework.ldap.ldif.support.SeparatorPolicy;
import org.springframework.ldap.schema.DefaultSchemaSpecification;
import org.springframework.ldap.schema.Specification;
import org.springframework.util.Assert;

/**
 * The {@link LdifParser LdifParser} is the main class of the {@link org.springframework.ldap.ldif} package.  
 * This class reads lines from a resource and assembles them into an {@link LdapAttributes LdapAttributes} object.
 * The {@link LdifParser LdifParser} does ignores <i>changetype</i> LDIF entries as their usefulness in the 
 * context of an application has yet to be determined.
 * <p>
 * <b>Design</b><br/>
 * {@link LdifParser LdifParser} provides the main interface for operation but requires three supporting classes to 
 * enable operation:
 * <ul>
 * 	<li>{@link SeparatorPolicy SeparatorPolicy} - establishes the mechanism by which lines are assembled into attributes.</li>
 * 	<li>{@link AttributeValidationPolicy AttributeValidationPolicy} - ensures that attributes are correctly structured prior to parsing.</li>
 * 	<li>{@link Specification Specification} - provides a mechanism by which object structure can be validated after assembly.</li>
 * </ul>
 * Together, these 4 classes read from the resource line by line and translate the data into objects for use.
 * <p>
 * <b>Usage</b><br/>
 * {@link #getRecord() getRecord()} reads the next available record from the resource.  Lines are read and 
 * passed to the {@link SeparatorPolicy SeparatorPolicy} for interpretation.  The parser continues to read
 * lines and appends them to the buffer until it encounters the start of a new attribute or an end of record
 * delimiter.  When the new attribute or end of record is encountered, the buffer is passed to the 
 * {@link AttributeValidationPolicy AttributeValidationPolicy} which ensures the buffer conforms to a valid 
 * attribute definition as defined in RFC2849 and returns an {@link org.springframework.ldap.core.LdapAttribute LdapAttribute} object 
 * which is then added to the record, an {@link LdapAttributes LdapAttributes} object.  Upon encountering the 
 * end of record, the record is validated by the {@link Specification Specification} policy and, 
 * if valid, returned to the requester.
 * <p>
 * <i>NOTE: By default, objects are not validated. If validation is required,
 * an appropriate specification object must be set.</i>
 * <p>
 * The parser requires the resource to be {@link #open() open()} prior to an invocation of {@link #getRecord() getRecord()}.  
 * {@link #hasMoreRecords() hasMoreRecords()} can be used to loop over the resource until all records have been 
 * retrieved.  Likewise, the {@link #reset() reset()} method will reset the resource.
 * <p>
 * Objects implementing the {@link javax.naming.directory.Attributes Attributes} interface are required to support a case sensitivity setting
 * which controls whether or not the attribute IDs of the object are case sensitive.  The {@link #caseInsensitive caseInsensitive}
 * setting of the {@link LdifParser LdifParser} is passed to the constructor of any {@link javax.naming.directory.Attributes Attributes} created. The
 * default value for this setting is true so that case insensitive objects are created.
 * 
 * @author Keith Barlow
 *
 */
public class LdifParser implements Parser, InitializingBean {

	private static final Log log = LogFactory.getLog(LdifParser.class);
	
	/**
	 * The resource to parse.
	 */
	private Resource resource;
	
	/**
	 * A BufferedReader to read the file.
	 */
	private BufferedReader reader;
	
	/**
	 * The SeparatorPolicy to use for interpreting attributes from the lines of the resource.
	 */
	private SeparatorPolicy separatorPolicy = new SeparatorPolicy();

	/**
	 * The AttributeValidationPolicy to use to interpret attributes.
	 */
	private AttributeValidationPolicy attributePolicy = new DefaultAttributeValidationPolicy();
	
	/**
	 * The RecordSpecification for validating records produced.
	 */
	private Specification<LdapAttributes> specification = new DefaultSchemaSpecification();
	
	/**
	 * This setting is used to control the case sensitivity of LdapAttribute objects returned by the parser.
	 */
	private boolean caseInsensitive = true;
	
	/**
	 * Default constructor.
	 */
	public LdifParser() {
		
	}

	/**
	 * Creates a LdifParser with the indicated case sensitivity setting.
	 * 
	 * @param caseInsensitive Case sensitivity setting for LdapAttributes objects returned by the parser.
	 */
	public LdifParser(boolean caseInsensitive) {
		this.caseInsensitive = caseInsensitive;
	}
	
	/**
	 * Creates an LdifParser for the specified resource with the provided case sensitivity setting.
	 * 
	 * @param resource The resource to parse.
	 * @param caseInsensitive Case sensitivity setting for LdapAttributes objects returned by the parser.
	 */
	public LdifParser(Resource resource, boolean caseInsensitive) {
		this.resource = resource;
		this.caseInsensitive = caseInsensitive;
	}
	
	/**
	 * Convenience constructor for resource specification.
	 * 
	 * @param resource The resource to parse.
	 */
	public LdifParser(Resource resource) {
		this.resource = resource;
	}
	
	/**
	 * Convenience constructor: accepts a File object.
	 * 
	 * @param file The file to parse.
	 */
	public LdifParser(File file) {
		this.resource = new FileSystemResource(file);
	}
	
	/**
	 * Set the separator policy.
	 * 
	 * The default separator policy should suffice for most needs.
	 * 
	 * @param separatorPolicy Separator policy.
	 */
	public void setSeparatorPolicy(SeparatorPolicy separatorPolicy) {
		this.separatorPolicy = separatorPolicy;
	}
	
	/**
	 * Policy object enforcing the rules for acceptable attributes.
	 * 
	 * @param avPolicy Attribute validation policy.
	 */
	public void setAttributeValidationPolicy(AttributeValidationPolicy avPolicy) {
		this.attributePolicy = avPolicy;
	}
	
	/**
	 * Policy object for enforcing rules to acceptable LDAP objects.
	 * 
	 * This policy may be used to enforce schema restrictions.
	 * @param specification
	 */
	public void setRecordSpecification(Specification<LdapAttributes> specification) {
		this.specification = specification;
	}

	public void setResource(Resource resource) {
		this.resource = resource;		
	}

	public void setCaseInsensitive(boolean caseInsensitive) {
		this.caseInsensitive = caseInsensitive;
	}
	
	public void open() throws IOException {
		Assert.notNull(resource, "Resource must be set.");
		reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));		
	}

	public boolean isReady() throws IOException {
		return reader.ready();
	}
	
	public void close() throws IOException {	
		if (resource.isOpen())
			reader.close();
	}

	public void reset() throws IOException {
		Assert.notNull(reader, "A reader has not been obtained.");
		reader.reset();
	}
	
	public boolean hasMoreRecords() throws IOException {
		return reader.ready();
	}
	
	public LdapAttributes getRecord() throws IOException {
		Assert.notNull(reader, "A reader must be obtained: parser not open.");
		
		if (!reader.ready()) {
			log.debug("Reader not ready!");
			return null;
		}
		
		LdapAttributes record = new LdapAttributes(caseInsensitive);
		StringBuilder builder = new StringBuilder();
		
		String line = reader.readLine();
		
		while(true) {			
						
			LineIdentifier identifier = separatorPolicy.assess(line);
			
			switch(identifier) {
				case NewRecord:
					log.trace("Starting new record.");
					//Start new record.
					builder = new StringBuilder(line);
					
					break;
				
				case Control:
					log.trace("'control' encountered.");
					
					//Log WARN and discard record.
					log.warn("LDIF change records have no implementation: record will be ignored.");					
					builder = null;
					record = null;
					
					break;
					
				case ChangeType:
					log.trace("'changetype' encountered.");
					
					//Log WARN and discard record.
					log.warn("LDIF change records have no implementation: record will be ignored.");					
					builder = null;
					record = null;
					
					break;
					
				case Attribute:
					//flush buffer.
					addAttributeToRecord(builder.toString(), record);

					log.trace("Starting new attribute.");
					//Start new attribute.
					builder = new StringBuilder(line);	
					
					break;
					
				case Continuation:
					log.trace("...appending line to buffer.");
					//Append line to buffer.
					builder.append(line.replaceFirst(" ", ""));
					
					break;					
				
				case EndOfRecord:
					log.trace("...done parsing record. (EndOfRecord)");
					
					//Validate record and return.
					if (record == null) return null;
					else {
						try {
							//flush buffer.
							addAttributeToRecord(builder.toString(), record);
							
							if (specification.isSatisfiedBy(record)) {
								log.debug("record parsed:\n" + record);
								return record;
								
							} else {
								throw new InvalidRecordFormatException("Record [dn: " + record.getDN() + "] does not conform to specification.");
							}
						} catch(NamingException e) {
							log.error(e);
							return null;
						}
					}
					
				default:
					//Take no action -- applies to VersionIdentifier, Comments, and voided records.
			}
			
			line = reader.readLine();
			
		}

	}
	
	private void addAttributeToRecord(String buffer, LdapAttributes record) {
		try {
			if (StringUtils.isNotEmpty(buffer) && record != null) {
				//Validate previous attribute and add to record.
				Attribute attribute = attributePolicy.parse(buffer);
					
				if (attribute.getID().equalsIgnoreCase("dn")) {
					log.trace("...adding DN to record.");
					
					String dn;
					if (attribute.get() instanceof byte[]) {
						dn = new String((byte[]) attribute.get());
					} else {
						dn = (String) attribute.get();
					}

					record.setDN(new DistinguishedName(dn));
					
				} else {
					log.trace("...adding attribute to record.");
					Attribute attr = record.get(attribute.getID());
					
					if (attr != null) {
						attr.add(attribute.get());
					} else {
						record.put(attribute);
					}
				}
			}			
		} catch (NamingException e) {
			log.error(e);
		} catch (NoSuchElementException e) {
			log.error(e);
		}
	}

	public void afterPropertiesSet() throws Exception {
		Assert.notNull(resource, "A resource to parse is required.");
		Assert.isTrue(resource.exists(), resource.getDescription() + ": resource does not exist!");		
		Assert.isTrue(resource.isReadable(), "Resource is not readable.");		
	}
		
}
