/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
package org.teiid.translator.ldap;

import java.util.List;

import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.ldap.LdapContext;

import org.teiid.core.util.StringUtil;
import org.teiid.language.*;
import org.teiid.language.Comparison.Operator;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.metadata.AbstractMetadataRecord;
import org.teiid.metadata.Column;
import org.teiid.translator.DataNotAvailableException;
import org.teiid.translator.TranslatorException;
import org.teiid.translator.UpdateExecution;



/**
 * Please see the user's guide for a full description of capabilties, etc.
 * 
 * Description/Assumptions:
 * 1. Table's name in source defines the base DN (or context) for the search.
 * Example: Table.NameInSource=ou=people,dc=gene,dc=com
 * 2. Column's name in source defines the LDAP attribute name.
 * [Default] If no name in source is defined, then we attempt to use the column name
 * as the LDAP attribute name.
 * 3. Since all of the underlying LDAP methods for adding/deleting/updating
 * require specification of the LDAP distinguished name (DN) to change, for all
 * corresponding MetaMatrix operations the DN must be specified (as the sole
 * item in the WHERE clause for UPDATE and DELETE operations, and in the list
 * of attributes to assign values in an INSERT operation * Responsible for update/insert/delete operations against LDAP
 */
public class LDAPUpdateExecution implements UpdateExecution {
	private LdapContext ldapConnection;
	private LdapContext ldapCtx;
	private Command command;

	public LDAPUpdateExecution(Command command, LdapContext ldapCtx) {
		this.ldapConnection = ldapCtx;
		this.command = command;
	}
	
	/** execute generic update-class (either an update, delete, or insert)
	 * operation and returns a count of affected rows.  Since underlying
	 * LDAP operations (and this connector) can modify at most one LDAP
	 * leaf context at a time, this will always return 1.  It will never
	 * actually return 0, because if an operation fails, a
	 * ConnectorException will be thrown instead.
	 * Note that really it should return 0 if a delete is performed on
	 * an entry that doesn't exist (but whose parent does exist), but
	 * since the underlying LDAP operation will return success for such a
	 * delete, we just blindly return 1.  To return 0 would mean performing
	 * a search for the entry first before deleting it (to confirm that it
	 * did exist prior to the delete), so right now we sacrifice accuracy
	 * here for the sake of efficiency.
	 */
	@Override
	public void execute() throws TranslatorException {
		// first make a copy of the initial LDAP context we got from
		// the connection.  The actual update-class operation will use
		// this copy.  This will enable the close and cancel methods
		// to stop any LDAP operations we are making by calling the
		// close() method of the copy context, without closing our
		// real connection to the LDAP server
		try {
			ldapCtx = (LdapContext)this.ldapConnection.lookup("");  //$NON-NLS-1$
		} catch (NamingException ne) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.createContextError",ne.getExplanation()); //$NON-NLS-1$
			throw new TranslatorException(ne, msg);
		}

		if (command instanceof Update) {
			executeUpdate();
		}
		else if (command instanceof Delete) {
			executeDelete();
		}
		else if (command instanceof Insert) {
			executeInsert();
		}
		else {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.incorrectCommandError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
	}
	
	@Override
	public int[] getUpdateCounts() throws DataNotAvailableException,
			TranslatorException {
		return new int[] {1};
	}


	// Private method to actually do an insert operation.  Per JNDI doc at
	// http://java.sun.com/products/jndi/tutorial/ldap/models/operations.html, JNDI method to add new entry to LDAP that does not contain a Java object is
	// DirContext.createSubContext(), so that is what is used here.
	//
	// The insert must include an element named "DN" (case insensitive)
	// which will be the fully qualified LDAP distinguished name of the 
	// entry to add.
	//
	// Also, while we make no effort to prevent insert operations that
	// break these rules, the underlying LDAP operation will fail (and
	// pass back an explanatory message, which we will return in a
	// ConnectorException, in the following cases:
	// -if the parent context for this entry does not exist in the directory
	// -if the insert does not specify values for all required attributes
	// of the class.  Since objectClass is required for all LDAP entries,
	// if it is not specified this condition will apply - and once it is
	// specified then all of the other required attributes for that
	// objectClass will of course also be required.
	//
	// TODO - maybe automatically specify objectClass based off of
	// Name/NameInSource RESTRICT property settings, like with read support
	private void executeInsert()
			throws TranslatorException {

		List<ColumnReference> insertElementList = ((Insert)command).getColumns();
		List<Expression> insertValueList = ((ExpressionValueSource)((Insert)command).getValueSource()).getValues();
		// create a new attribute list with case ignored in attribute
		// names
		Attributes insertAttrs = new BasicAttributes(true);
		String distinguishedName = null;
		// The IInsert interface uses separate List objects for
		// the element names and values to be inserted, limiting
		// the potential for code reuse in reading them (since all
		// other interfaces use ICriteria-based mechanisms for such
		// input).
		for (int i=0; i < insertElementList.size(); i++) {
			ColumnReference insertElement = insertElementList.get(i);
			// call utility class to get NameInSource/Name of element
			String nameInsertElement = getNameFromElement(insertElement);
			// special handling for DN attribute - use it to set
			// distinguishedName value.
			Expression literal = insertValueList.get(i);
			if (nameInsertElement.toUpperCase().equals("DN")) {  //$NON-NLS-1$
				Object insertValue = ((Literal)literal).getValue();
				if (insertValue == null) { 
		            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.columnSourceNameDNNullError"); //$NON-NLS-1$
					throw new TranslatorException(msg);
				}
				if (!(insertValue instanceof java.lang.String)) {
		            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.columnSourceNameDNTypeError"); //$NON-NLS-1$
					throw new TranslatorException(msg);
				}
				distinguishedName = (String)insertValue;
			}
			// for other attributes specified in the insert command,
			// create a new 
			else {
				Attribute insertAttr = createBasicAttribute(nameInsertElement,
						literal, insertElement.getMetadataObject());
				insertAttrs.put(insertAttr);
			}
		}
		// if the DN is not specified, we don't know enough to attempt
		// the LDAP add operation, so throw an exception
		if (distinguishedName == null) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.noInsertSourceNameDNError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		// just try to create a new LDAP entry using the DN and
		// attributes specified in the INSERT operation.  If it isn't
		// legal, we'll get a NamingException back, whose explanation
		// we'll return in a ConnectorException
		try {
			ldapCtx.createSubcontext(distinguishedName, insertAttrs);
		} catch (NamingException ne) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.insertFailed",distinguishedName,ne.getExplanation()); //$NON-NLS-1$
			throw new TranslatorException(ne, msg);
		} catch (Exception e) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.insertFailedUnexpected",distinguishedName); //$NON-NLS-1$
			throw new TranslatorException(e, msg);
		}
	}

	static Attribute createBasicAttribute(String id,
			Expression expr, Column col) {
		Attribute attr = new BasicAttribute(id);
		if (expr instanceof org.teiid.language.Array) {
			List<Expression> exprs = ((org.teiid.language.Array)expr).getExpressions();
			for (Expression val : exprs) {
				Literal l = (Literal)val;
				if (l.getValue() != null) {
					attr.add(IQueryToLdapSearchParser.getLiteralString(l));
				}
			}
		} else {
			Literal l = (Literal)expr;
			Object insertValue = null;
			if (l.getValue() != null) {
				if (LDAPQueryExecution.MULTIVALUED_CONCAT.equalsIgnoreCase(col.getDefaultValue())) {
					List<String> vals = StringUtil.split(l.getValue().toString(), "?"); //$NON-NLS-1$
					for (String val : vals) {
						attr.add(val);
					}
					return attr;
				}
				insertValue = IQueryToLdapSearchParser.getLiteralString(l);
			}
			attr.add(insertValue);
		}
		return attr;
	}

	// Private method to actually do a delete operation.  Per JNDI doc at
	// http://java.sun.com/products/jndi/tutorial/ldap/models/operations.html, 
	// a good JNDI method to delete an entry to LDAP is
	// DirContext.destroySubContext(), so that is what is used here.
	//
	// The delete criteria must include only an equals comparison
	// on the "DN" column ("WHERE DN='cn=John Doe,ou=people,dc=company,dc=com'") 
	// Note that the underlying LDAP operations here return successfully
	// even if the named entry doesn't exist (as long as its parent does
	// exist).
	private void executeDelete()
			throws TranslatorException {

		Condition criteria = ((Delete)command).getWhere();

		// since we have the exact same processing rules for criteria
		// for updates and deletes, we use a common private method to do this.
		// note that this private method will throw a ConnectorException
		// for illegal criteria, which we deliberately don't catch
		// so it gets passed on as is.
		String distinguishedName = getDNFromCriteria(criteria);

		// just try to delete an LDAP entry using the DN
		// specified in the DELETE operation.  If it isn't
		// legal, we'll get a NamingException back, whose explanation
		// we'll return in a ConnectorException
		try {
			ldapCtx.destroySubcontext(distinguishedName);
		} catch (NamingException ne) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.deleteFailed",distinguishedName,ne.getExplanation()); //$NON-NLS-1$
			throw new TranslatorException(msg);
		// don't remember why I added this generic catch of Exception,
		// but it does no harm...
		} catch (Exception e) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.deleteFailedUnexpected",distinguishedName); //$NON-NLS-1$
			throw new TranslatorException(e, msg);
		}
	}

	// Private method to actually do an update operation.  Per JNDI doc at
	// http://java.sun.com/products/jndi/tutorial/ldap/models/operations.html, 
	// the JNDI method to use to update an entry to LDAP is one of the
	// DirContext.modifyAttributes() methods that takes ModificationItem[]
	// as a parameter, so that is what is used here.
	// Note that this method does not allow for changing of the DN - to
	// implement that we would need to use Context.rename().  Since right
	// now we only call modifyAttributes(), and don't check for the DN
	// in the list of updates, we will attempt to update the DN using
	// modifyAttributes(), and let the LDAP server fail the request (and
	// send us the explanation for the failure, which is returned in
	// a ConnectorException)
	//
	// The update criteria must include only an equals comparison
	// on the "DN" column ("WHERE DN='cn=John Doe,ou=people,dc=company,dc=com'") 
	private void executeUpdate()
			throws TranslatorException {

		List<SetClause> updateList = ((Update)command).getChanges();
		Condition criteria = ((Update)command).getWhere();

		// since we have the exact same processing rules for criteria
		// for updates and deletes, we use a common private method to do this.
		// note that this private method will throw a ConnectorException
		// for illegal criteria, which we deliberately don't catch
		// so it gets passed on as is.
		String distinguishedName = getDNFromCriteria(criteria);
		

		// this will be the list of modifications to attempt.  Since
		// we currently blindly try all the updates the query
		// specifies, right now this is the same size as the updateList.
		// When we start filtering out DN changes (which would need to
		// be performed separately using Context.rename()), we will
		// need to account for this in determining this list size. 
		ModificationItem[] updateMods = new ModificationItem[updateList.size()];
		// iterate through the supplied list of updates (each of
		// which is an ICompareCriteria with an IElement on the left
		// side and an IExpression on the right, per the Connector
		// API).
		for (int i=0; i < updateList.size(); i++) {
			SetClause setClause = updateList.get(i);
			// trust that connector API is right and left side
			// will always be an IElement
			ColumnReference leftElement = setClause.getSymbol();
			// call utility method to get NameInSource/Name for element
			String nameLeftElement = getNameFromElement(leftElement);
			// get right expression - if it is not a literal we
			// can't handle that so throw an exception
			Expression rightExpr = setClause.getValue();
			if (!(rightExpr instanceof Literal) && !(rightExpr instanceof org.teiid.language.Array)) { 
	            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.valueNotLiteralError",nameLeftElement); //$NON-NLS-1$
				throw new TranslatorException(msg);
			}
			// add in the modification as a replacement - meaning
			// any existing value(s) for this attribute will
			// be replaced by the new value.  If the attribute
			// didn't exist, it will automatically be created
			// TODO - since null is a valid attribute
			// value, we don't do any special handling of it right
			// now.  But maybe null should mean to delete an
			// attribute?
			Attribute attribute = createBasicAttribute(nameLeftElement, rightExpr, leftElement.getMetadataObject());
	        updateMods[i] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute);
		}
		// just try to update an LDAP entry using the DN and
		// attributes specified in the UPDATE operation.  If it isn't
		// legal, we'll get a NamingException back, whose explanation
		// we'll return in a ConnectorException
		try {
			ldapCtx.modifyAttributes(distinguishedName, updateMods);
		} catch (NamingException ne) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.updateFailed",distinguishedName,ne.getExplanation()); //$NON-NLS-1$
			throw new TranslatorException(ne, msg);
		// don't remember why I added this generic catch of Exception,
		// but it does no harm...
		} catch (Exception e) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.updateFailedUnexpected",distinguishedName); //$NON-NLS-1$
			throw new TranslatorException(e, msg);
		}
	}

	// private method for extracting the distinguished name from
	// the criteria, which must include only an equals comparison
	// on the "DN" column ("WHERE DN='cn=John Doe,ou=people,dc=company,dc=com'") 
	// most of this code is to check the criteria to make sure it is in
	// this form and throw an appropriate exception if it is not
	// since there is no way to specify this granularity of criteria
	// right now in the connector capabilities
	private String getDNFromCriteria(Condition criteria)
			throws TranslatorException {
		if (criteria == null) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaEmptyError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		if (!(criteria instanceof Comparison)) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaNotSimpleError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		Comparison compareCriteria = (Comparison)criteria;	
		if (compareCriteria.getOperator() != Operator.EQ) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaNotEqualsError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		Expression leftExpr = compareCriteria.getLeftExpression();
		if (!(leftExpr instanceof ColumnReference)) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaLHSNotElementError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		// call utility method to get NameInSource/Name for element
		String nameLeftExpr = getNameFromElement((ColumnReference)leftExpr);
		if (!(nameLeftExpr.toUpperCase().equals("DN"))) {   //$NON-NLS-1$
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaSrcColumnError",nameLeftExpr); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		Expression rightExpr = compareCriteria.getRightExpression();
		if (!(rightExpr instanceof Literal)) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaRHSNotLiteralError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		Object valueRightExpr = ((Literal)rightExpr).getValue();
		if (!(valueRightExpr instanceof java.lang.String)) {
            final String msg = LDAPPlugin.Util.getString("LDAPUpdateExecution.criteriaRHSNotStringError"); //$NON-NLS-1$
			throw new TranslatorException(msg);
		}
		return (String)valueRightExpr;
	}

	// This is an exact copy of the method with the same name in
	// IQueryToLdapSearchParser - really should be in a utility class
	private String getNameFromElement(ColumnReference e) {
		AbstractMetadataRecord mdObject = e.getMetadataObject();
		if (mdObject == null) {
			return "";  //$NON-NLS-1$
		}
		return mdObject.getSourceName();
	}


	// cancel here by closing the copy of the ldap context (if it was
	// initialized, which is only true if execute() was previously called)
	// calling close on already closed context is safe per
	// javax.naming.Context javadoc so we won't worry about this also
	// happening in our close method
	public void cancel() throws TranslatorException {
		close();
	}

	// close here by closing the copy of the ldap context (if it was
	// initialized, which is only true if execute() was previously called)
	// calling close on already closed context is safe per
	// javax.naming.Context javadoc so we won't worry about this also
	// happening in our close method
	public void close() {
		try {
			if(ldapCtx != null) {
				ldapCtx.close();
			}
		} catch (NamingException ne) {
            LogManager.logWarning(LogConstants.CTX_CONNECTOR, LDAPPlugin.Util.gs(LDAPPlugin.Event.TEIID12003, ne.getExplanation()));
		}
	}

}
	



				
				
				
				
				
							
