/* * 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. */ /** * * 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 * [Optional] The table's name in source can also define a search scope. Append * a "?" character as a delimiter to the base DN, and add the search scope string. * The following scopes are available: * SUBTREE_SCOPE * ONELEVEL_SCOPE * OBJECT_SCOPE * [Default] LDAPConnectorConstants.ldapDefaultSearchScope * is the default scope used, if no scope is defined (currently, ONELEVEL_SCOPE). * * 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. * * * TODO: Implement paged searches -- the LDAP server must support VirtualListViews. * TODO: Implement cancel. * TODO: Add Sun/Netscape implementation, AD/OpenLDAP implementation. * * * Note: * Greater than is treated as >= * Less-than is treater as <= * If an LDAP entry has more than one entry for an attribute of interest (e.g. a select item), we only return the * first occurrance. The first occurance is not predictably the same each time, either, according to the LDAP spec. * If an attribute is not present, we return the empty string. Arguably, we could throw an exception. * * Sun LDAP won't support Sort Orders for very large datasets. So, we've set the sorting to NONCRITICAL, which * allows Sun to ignore the sort order. This will result in the results to come back as unsorted, without any error. * * Removed support for ORDER BY for two reasons: * 1: LDAP appears to have a limit to the number of records that * can be server-side sorted. When the limit is reached, two things can happen: * a. If sortControl is set to CRITICAL, then the search fails. * b. If sortControl is NONCRITICAL, then the search returns, unsorted. * We'd like to support ORDER BY, no matter how large the size, so we turn it off, * and allow MetaMatrix to do it for us. * 2: Supporting ORDER BY appears to negatively effect the query plan * when cost analysis is used. We stop using dependent queries, and start * using inner joins. * */ package org.teiid.translator.ldap; import java.io.IOException; import java.lang.reflect.Array; import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import javax.naming.InvalidNameException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.SizeLimitExceededException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.Control; import javax.naming.ldap.LdapContext; import javax.naming.ldap.LdapName; import javax.naming.ldap.PagedResultsControl; import javax.naming.ldap.PagedResultsResponseControl; import javax.naming.ldap.Rdn; import javax.naming.ldap.SortControl; import javax.naming.ldap.SortKey; import org.teiid.core.types.ArrayImpl; import org.teiid.logging.LogConstants; import org.teiid.logging.LogManager; import org.teiid.metadata.Column; import org.teiid.translator.ExecutionContext; import org.teiid.translator.ResultSetExecution; import org.teiid.translator.TranslatorException; import org.teiid.translator.TypeFacility; /** * LDAPSyncQueryExecution is responsible for executing an LDAP search * corresponding to a read-only "select" query. */ public class LDAPQueryExecution implements ResultSetExecution { static final String MULTIVALUED_CONCAT = "multivalued-concat"; //$NON-NLS-1$ static final String delimiter = "?"; //$NON-NLS-1$ private LDAPSearchDetails searchDetails; private LdapContext ldapCtx; private NamingEnumeration<?> searchEnumeration; private LDAPExecutionFactory executionFactory; private ExecutionContext executionContext; private SearchControls ctrls; private int resultCount; private Iterator<List<Object>> unwrapIterator; private int unwrapPos = -1; /** * Constructor * @param executionMode the execution mode. * @param ctx the execution context. * @param logger the ConnectorLogger * @param connection the LDAP Context */ public LDAPQueryExecution(LdapContext ldapContext,LDAPSearchDetails search, SearchControls searchControls, LDAPExecutionFactory factory,ExecutionContext context) { this.searchDetails = search; this.ldapCtx = ldapContext; this.ctrls = searchControls; this.executionFactory = factory; this.executionContext = context; } /** * method to execute the supplied query * @param query the query object. * @param maxBatchSize the max batch size. */ @Override public void execute() throws TranslatorException { String ctxName = this.searchDetails.getContextName(); String filter = this.searchDetails.getContextFilter(); if (ctxName == null || filter == null || this.ctrls == null) { throw new TranslatorException("Search context, filter, or controls were null. Cannot execute search."); //$NON-NLS-1$ } ArrayList<Column> attributeList = searchDetails.getElementList(); //determine if there is an array value to unwrap for (int i = 0; i < attributeList.size(); i++) { Column col = attributeList.get(i); if (Boolean.valueOf(col.getProperty(LDAPExecutionFactory.UNWRAP, false))) { if (unwrapPos > -1) { throw new TranslatorException(LDAPPlugin.Util.gs(LDAPPlugin.Event.TEIID12014, col, attributeList.get(unwrapPos))); } unwrapPos = i; } } setRequestControls(null); // Execute the search. executeSearch(); } /** * Set the standard request controls */ private void setRequestControls(byte[] cookie) throws TranslatorException { List<Control> ctrl = new ArrayList<Control>(); SortKey[] keys = searchDetails.getSortKeys(); try { if (keys != null) { ctrl.add(new SortControl(keys, Control.NONCRITICAL)); } if (this.executionFactory.usePagination()) { ctrl.add(new PagedResultsControl(this.executionContext.getBatchSize(), cookie, Control.CRITICAL)); } if (!ctrl.isEmpty()) { this.ldapCtx.setRequestControls(ctrl.toArray(new Control[ctrl.size()])); LogManager.logTrace(LogConstants.CTX_CONNECTOR, "Sort/pagination controls were created successfully."); //$NON-NLS-1$ } } catch (NamingException ne) { final String msg = LDAPPlugin.Util.getString("LDAPSyncQueryExecution.setControlsError") + //$NON-NLS-1$ " : "+ne.getExplanation(); //$NON-NLS-1$ throw new TranslatorException(ne, msg); } catch(IOException e) { throw new TranslatorException(e); } } /** * Perform the LDAP search against the subcontext, using the filter and * search controls appropriate to the query and model metadata. */ private void executeSearch() throws TranslatorException { String filter = searchDetails.getContextFilter(); try { searchEnumeration = this.ldapCtx.search("", filter, ctrls); //$NON-NLS-1$ } catch (NamingException ne) { final String msg = LDAPPlugin.Util.getString("LDAPSyncQueryExecution.execSearchError"); //$NON-NLS-1$ throw new TranslatorException(ne, msg + " : " + ne.getExplanation()); //$NON-NLS-1$ } catch(Exception e) { final String msg = LDAPPlugin.Util.getString("LDAPSyncQueryExecution.execSearchError"); //$NON-NLS-1$ throw new TranslatorException(e, msg); } } // GHH 20080326 - attempt to implement cancel here. First try to // close the searchEnumeration, then the search context. // We are very conservative when closing the enumeration // but less so when closing context, since it is safe to call close // on contexts multiple times @Override public void cancel() throws TranslatorException { close(); } // GHH 20080326 - replaced existing implementation with the same // code as used by cancel method. First try to // close the searchEnumeration, then the search context // We are very conservative when closing the enumeration // but less so when closing context, since it is safe to call close // on contexts multiple times @Override public void close() { if (searchEnumeration != null) { try { searchEnumeration.close(); } catch (Exception e) { } // catch everything, because NamingEnumeration has undefined behavior if it previously hit an exception } if (ldapCtx != null) { try { ldapCtx.close(); } catch (NamingException ne) { LogManager.logWarning(LogConstants.CTX_CONNECTOR, LDAPPlugin.Util.gs(LDAPPlugin.Event.TEIID12003, ne.getExplanation())); } } } /** * Fetch the next batch of data from the LDAP searchEnumerationr result. * @return the next Batch of results. */ // GHH 20080326 - set all batches as last batch after an exception // is thrown calling a method on the enumeration. Per Javadoc for // javax.naming.NamingEnumeration, enumeration is invalid after an // exception is thrown - by setting last batch indicator we prevent // it from being used again. // GHH 20080326 - also added return of explanation for generic // NamingException public List<?> next() throws TranslatorException { try { if (unwrapIterator != null) { if (unwrapIterator.hasNext()) { return unwrapIterator.next(); } unwrapIterator = null; } // The search has been executed, so process up to one batch of // results. List<?> result = null; while (result == null && searchEnumeration != null && searchEnumeration.hasMore()) { SearchResult searchResult = (SearchResult) searchEnumeration.next(); try { result = getRow(searchResult); } catch (InvalidNameException e) { } } if (result == null && this.executionFactory.usePagination()) { byte[] cookie = null; Control[] controls = ldapCtx.getResponseControls(); if (controls != null) { for (int i = 0; i < controls.length; i++) { if (controls[i] instanceof PagedResultsResponseControl) { PagedResultsResponseControl prrc = (PagedResultsResponseControl)controls[i]; cookie = prrc.getCookie(); } } } if (cookie == null) { return null; } setRequestControls(cookie); executeSearch(); return next(); } if (result != null) { resultCount++; } return result; } catch (SizeLimitExceededException e) { if (resultCount != searchDetails.getCountLimit()) { String msg = LDAPPlugin.Util.gs(LDAPPlugin.Event.TEIID12008); TranslatorException te = new TranslatorException(e, msg); if (executionFactory.isExceptionOnSizeLimitExceeded()) { throw te; } this.executionContext.addWarning(te); LogManager.logWarning(LogConstants.CTX_CONNECTOR, e, msg); } return null; // GHH 20080326 - if size limit exceeded don't try to read more results } catch (NamingException ne) { throw new TranslatorException(ne, LDAPPlugin.Util.gs("ldap_error")); //$NON-NLS-1$ } } /** * Create a row using the searchResult and add it to the supplied batch. * @param batch the supplied batch * @param result the search result * @throws InvalidNameException */ // GHH 20080326 - added fetching of DN of result, for directories that // do not include it as an attribute private List<?> getRow(SearchResult result) throws TranslatorException, InvalidNameException { Attributes attrs = result.getAttributes(); ArrayList<Column> attributeList = searchDetails.getElementList(); final List<Object> row = new ArrayList<Object>(attributeList.size()); for (int i = 0; i < attributeList.size(); i++) { Column col = attributeList.get(i); Object val = getValue(col, result, attrs, i == unwrapPos); // GHH 20080326 - added resultDN parameter to call row.add(val); } if (unwrapPos > -1) { Object toUnwrap = row.get(unwrapPos); if (toUnwrap == null) { return row; //missing value } if (toUnwrap instanceof ArrayImpl) { final Object[] val = ((ArrayImpl) toUnwrap).getValues(); if (val.length == 0) { row.set(unwrapPos, null); //empty value } else { unwrapIterator = new Iterator<List<Object>>() { int i = 0; @Override public boolean hasNext() { return i < val.length; } @Override public List<Object> next() { List<Object> newRow = new ArrayList<Object>(row); newRow.set(unwrapPos, val[i++]); return newRow; } @Override public void remove() { } }; if (unwrapIterator.hasNext()) { return unwrapIterator.next(); } } } } return row; } /** * Add Result to Row * @param modelElement the model element * @param attrs the attributes * @param row the row * @throws InvalidNameException */ // GHH 20080326 - added resultDistinguishedName to method signature. If // there is an element in the model named "DN" and there is no attribute // with this name in the search result, we return this new parameter // value for that column in the result // GHH 20080326 - added handling of ClassCastException when non-string // attribute is returned private Object getValue(Column modelElement, SearchResult result, Attributes attrs, boolean unwrap) throws TranslatorException, InvalidNameException { String modelAttrName = modelElement.getSourceName(); Class<?> modelAttrClass = modelElement.getJavaType(); String multivalAttr = modelElement.getDefaultValue(); if(modelAttrName == null) { final String msg = LDAPPlugin.Util.getString("LDAPSyncQueryExecution.nullAttrError"); //$NON-NLS-1$ throw new TranslatorException(msg); } Attribute resultAttr = attrs.get(modelAttrName); // If the attribute is not present, we return NULL. if(resultAttr == null) { // GHH 20080326 - return DN from input parameter // if DN attribute is not present in search result if (modelAttrName.equalsIgnoreCase("DN")) { //$NON-NLS-1$ return result.getNameInNamespace(); } return null; } Object objResult = null; try { if(TypeFacility.RUNTIME_TYPES.STRING.equals(modelAttrClass) && MULTIVALUED_CONCAT.equalsIgnoreCase(multivalAttr)) { // mpw 5/09 // Order the multi-valued attrs alphabetically before creating a single string, // using the delimiter to separate each token ArrayList<String> multivalList = new ArrayList<String>(); NamingEnumeration<?> attrNE = resultAttr.getAll(); int length = 0; while(attrNE.hasMore()) { String val = (String)attrNE.next(); multivalList.add(val); length += ((val==null?0:val.length()) + 1); } Collections.sort(multivalList); StringBuilder multivalSB = new StringBuilder(length); Iterator<String> itr = multivalList.iterator(); while(itr.hasNext()) { multivalSB.append(itr.next()); if (itr.hasNext()) { multivalSB.append(delimiter); } } return multivalSB.toString(); } if (modelAttrClass.isArray()) { return getArray(modelAttrClass.getComponentType(), resultAttr, modelElement, modelAttrName); } if (unwrap && resultAttr.size() > 1) { return getArray(modelAttrClass, resultAttr, modelElement, modelAttrName); } //just a single value objResult = resultAttr.get(); } catch (NamingException ne) { final String msg = LDAPPlugin.Util.gs(LDAPPlugin.Event.TEIID12004, modelAttrName) +" : "+ne.getExplanation(); //$NON-NLS-1$m LogManager.logWarning(LogConstants.CTX_CONNECTOR, msg); throw new TranslatorException(ne, msg); } return convertSingleValue(modelElement, modelAttrName, modelAttrClass, objResult); } private Object convertSingleValue(Column modelElement, String modelAttrName, Class<?> modelAttrClass, Object objResult) throws TranslatorException, InvalidNameException { if (objResult == null) { return null; } // GHH 20080326 - if attribute is not a string or empty, just // return null. if (!(objResult instanceof String)) { return objResult; } String strResult = (String)objResult; // MPW - 3.9.07 - Also return NULL when attribute is unset or empty string. // There is no way to differentiate between being unset and being the empty string. if(strResult.equals("")) { //$NON-NLS-1$ return null; } // MPW: 3-11-07: Added support for java.lang.Integer conversion. if(TypeFacility.RUNTIME_TYPES.TIMESTAMP.equals(modelAttrClass)) { String timestampFormat = modelElement.getFormat(); if(timestampFormat == null) { timestampFormat = LDAPConnectorConstants.ldapTimestampFormat; } SimpleDateFormat dateFormat = new SimpleDateFormat(timestampFormat); try { Date dateResult = dateFormat.parse(strResult); Timestamp tsResult = new Timestamp(dateResult.getTime()); return tsResult; } catch(ParseException pe) { throw new TranslatorException(pe, LDAPPlugin.Util.getString("LDAPSyncQueryExecution.timestampParseFailed", modelAttrName)); //$NON-NLS-1$ } // TODO: Extend support for more types in the future. // Specifically, add support for byte arrays, since that's actually supported // in the underlying data source. } //extract rdn String type = modelElement.getProperty(LDAPExecutionFactory.RDN_TYPE, false); if (type != null) { String prefix = modelElement.getProperty(LDAPExecutionFactory.DN_PREFIX, false); LdapName name = new LdapName(strResult); if (prefix != null) { if (!name.getPrefix(name.size() - 1).toString().equals(prefix)) { throw new InvalidNameException(); } } else if (name.size() > 1){ throw new InvalidNameException(); } Rdn rdn = name.getRdn(name.size() - 1); if (!rdn.getType().equals(type)) { throw new InvalidNameException(); } return rdn.getValue(); } return strResult; //the Teiid type conversion logic will handle refine from here if necessary } private ArrayImpl getArray(Class<?> componentType, Attribute resultAttr, Column modelElement, String modelAttrName) throws NamingException, TranslatorException { ArrayList<Object> multivalList = new ArrayList<Object>(); NamingEnumeration<?> attrNE = resultAttr.getAll(); int length = 0; while(attrNE.hasMore()) { try { multivalList.add(convertSingleValue(modelElement, modelAttrName, componentType, attrNE.next())); length++; } catch (InvalidNameException e) { //just ignore } } Object[] values = (Object[]) Array.newInstance(componentType, length); ArrayImpl value = new ArrayImpl(multivalList.toArray(values)); return value; } // for testing. LDAPSearchDetails getSearchDetails() { return this.searchDetails; } }