/*
* 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()));
}
}
}