/* * ==================== * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of the Common Development * and Distribution License("CDDL") (the "License"). You may not use this file * except in compliance with the License. * * You can obtain a copy of the License at * http://IdentityConnectors.dev.java.net/legal/license.txt * See the License for the specific language governing permissions and limitations * under the License. * * When distributing the Covered Code, include this CDDL Header Notice in each file * and include the License file at identityconnectors/legal/license.txt. * If applicable, add the following below this CDDL Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * ==================== * Portions Copyrighted 2013-2014 ForgeRock AS * Portions Copyrighted 2014 Evolveum */ package org.identityconnectors.ldap.search; import static org.identityconnectors.common.StringUtil.isNotBlank; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.Control; import javax.naming.ldap.LdapContext; import javax.naming.ldap.SortControl; import javax.naming.ldap.SortResponseControl; import org.identityconnectors.common.Base64; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.SortKey; import org.identityconnectors.ldap.LdapConnection; public class VlvIndexSearchStrategy extends LdapSearchStrategy { private static Log log; private OperationOptions options; private final String vlvDefaultSortAttr; private String sortOrderingRuleID; private final int blockSize; private int index; private int lastListSize; private byte[] cookie; static synchronized void setLog(Log log) { VlvIndexSearchStrategy.log = log; } synchronized static Log getLog() { if (log == null) { log = Log.getLog(VlvIndexSearchStrategy.class); } return log; } public VlvIndexSearchStrategy(OperationOptions options, String vlvDefaultSortAttr, String sortOrderingRuleID, int blockSize) { this.options = options; this.vlvDefaultSortAttr = isNotBlank(vlvDefaultSortAttr) ? vlvDefaultSortAttr : "uid"; this.sortOrderingRuleID = sortOrderingRuleID; this.blockSize = blockSize; } @Override public void doSearch(LdapConnection conn, List<String> baseDNs, String query, SearchControls searchControls, LdapSearchResultsHandler handler) throws IOException, NamingException { getLog().ok("Searching in {0} with filter {1} and {2}", baseDNs, query, searchControlsToString(searchControls)); Iterator<String> baseDNIter = baseDNs.iterator(); boolean proceed = true; LdapContext ctx = conn.getInitialContext().newInstance(null); try { while (baseDNIter.hasNext() && proceed) { proceed = searchBaseDN(conn, ctx, baseDNIter.next(), query, searchControls, handler); } } finally { ctx.close(); } } private boolean searchBaseDN(LdapConnection conn, LdapContext ctx, String baseDN, String query, SearchControls searchControls, LdapSearchResultsHandler handler) throws IOException, NamingException { getLog().ok("New VLV search in {0}", baseDN); boolean continueFlag = true; index = 1; if (options != null && options.getPagedResultsOffset() != null) { index = options.getPagedResultsOffset(); } Integer numberOfEntriesToReturn = null; // null means "as many as there are" if (options != null && options.getPageSize() != null) { numberOfEntriesToReturn = options.getPageSize(); } String vlvSortAttr = vlvDefaultSortAttr; boolean ascendingOrder = true; if (options != null && options.getSortKeys() != null && options.getSortKeys().length > 0) { if (options.getSortKeys().length > 1) { log.warn("Multiple sort keys are not supported"); } SortKey sortKey = options.getSortKeys()[0]; vlvSortAttr = sortKey.getField(); ascendingOrder = sortKey.isAscendingOrder(); } lastListSize = 0; cookie = null; if (options != null && options.getPagedResultsCookie() != null) { cookie = Base64.decode(options.getPagedResultsCookie()); } String lastResultName = null; int numberOfResutlsReturned = 0; for (;;) { javax.naming.ldap.SortKey ldapSortKey = new javax.naming.ldap.SortKey(vlvSortAttr, ascendingOrder, sortOrderingRuleID); SortControl sortControl = new SortControl(new javax.naming.ldap.SortKey[]{ldapSortKey}, Control.CRITICAL); int afterCount = blockSize - 1; if (numberOfEntriesToReturn != null && (numberOfResutlsReturned + afterCount + 1 > numberOfEntriesToReturn)) { afterCount = numberOfEntriesToReturn - numberOfResutlsReturned - 1; } VirtualListViewRequestControl vlvControl = new VirtualListViewRequestControl(0, afterCount, index, lastListSize, cookie, Control.CRITICAL); ctx.setRequestControls(new Control[] { sortControl, vlvControl }); // Need to process the response controls, which are available after // all results have been processed, before sending anything to the caller // (because processing the response controls might throw exceptions that // invalidate anything we might have sent otherwise). // So storing the results before actually sending them to the handler. List<SearchResult> resultList = new ArrayList<SearchResult>(blockSize); if (getLog().isOk()) { getLog().ok("LDAP search request: VLV( target = {0}, lastListSize = {1}, afterCount = {2}, cookie = {3} )," + " SSS( attr = {4}, ascending = {5}, ordering = {6} )", index, lastListSize, afterCount, Base64.encode(cookie), vlvSortAttr, ascendingOrder, sortOrderingRuleID); } NamingEnumeration<SearchResult> results = ctx.search(baseDN, query, searchControls); int resultCount = 0; try { while (results.hasMore()) { SearchResult result = results.next(); resultCount++; boolean overlap = false; if (lastResultName != null) { if (lastResultName.equals(result.getName())) { getLog().warn("Working around rounding error overlap at index {0} (name={1})", index, lastResultName); overlap = true; } lastResultName = null; } if (!overlap) { resultList.add(result); } } } finally { results.close(); } getLog().ok("LDAP search response: {0} results returned, reduced to {1}", resultCount, resultList.size()); processResponseControls(ctx.getResponseControls()); SearchResult result = null; Iterator<SearchResult> resultIter = resultList.iterator(); while (resultIter.hasNext()) { result = resultIter.next(); index++; numberOfResutlsReturned++; if (!handler.handle(baseDN, result)) { getLog().ok("Ending VLV search because handler returned false"); continueFlag = false; break; } } if (!continueFlag) { break; } if (result != null) { lastResultName = result.getName(); } getLog().ok("Handling of results completed, {0} resutls handled, index {1} (lastResultName={2})", numberOfResutlsReturned, index, lastResultName); if (index > lastListSize) { getLog().ok("Ending VLV search because index ({0}) went over list size ({1})", index, lastListSize); break; } if (numberOfEntriesToReturn != null && numberOfEntriesToReturn <= numberOfResutlsReturned) { getLog().ok("Ending VLV search because enough entries already returned"); break; } // DSEE seems to only have a single VLV index (although it claims to support more). // It returns at the server content count the sum of sizes of all indexes, // but it only returns the entries in the base context we are asking for. // So, in this case, index will never reach lastListSize. To avoid an infinite loop, // ending search if we received no results in the last iteration. if (resultList.isEmpty()) { getLog().warn("Ending VLV search because received no results"); break; } } if (options != null && options.getPagedResultsOffset() != null) { // If there was an offset then it is likely that this search will continue. Therefore do NOT close the search yet. } else { // Close the connection so the server can free any allocated resources. // The connection will automatically reconnect on the next use. conn.close(); } return continueFlag; } private void processResponseControls(Control[] controls) throws NamingException { if (controls != null) { for (Control control : controls) { if (control instanceof SortResponseControl) { SortResponseControl sortControl = (SortResponseControl) control; if (!sortControl.isSorted() || (sortControl.getResultCode() != 0)) { throw sortControl.getException(); } } if (control.getID().equalsIgnoreCase(VirtualListViewResponseControl.OID)) { try { VirtualListViewResponseControl vlvResponse = new VirtualListViewResponseControl(control.getID(), control.isCritical(), control.getEncodedValue()); byte[] value = control.getEncodedValue(); int offset = vlvResponse.getTargetPosition(); lastListSize = vlvResponse.getContentCount(); int code = vlvResponse.getVirtualListViewResult(); cookie = vlvResponse.getContextID(); if (getLog().isOk()) { getLog().ok("Response control: offset = {0}, lastListSize = {1}, cookie = {2}", offset, lastListSize, Base64.encode(cookie)); } if (code != 0) { throw new NamingException("The view operation has failed on LDAP server, error="+code); } } catch (IOException ex) { getLog().error("Can't decode response control"); } } } } } @Override public String getPagedResultsCookie() { if (cookie == null) { return null; } return Base64.encode(cookie); } @Override public int getRemainingPagedResults() { return lastListSize; } }