/* * SSSync, a Simple and Stupid Synchronizer for data with multi-valued attributes * Copyright (C) 2014 Ludovic Pouzenc * * This file is part of SSSync. * * SSSync is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * SSSync 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with SSSync. If not, see */ package data.io.ldap; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import com.unboundid.ldap.sdk.Attribute; import com.unboundid.ldap.sdk.Filter; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.SearchRequest; import com.unboundid.ldap.sdk.SearchResult; import com.unboundid.ldap.sdk.SearchResultEntry; import com.unboundid.ldap.sdk.SearchResultListener; import com.unboundid.ldap.sdk.SearchResultReference; import com.unboundid.ldap.sdk.SearchScope; import data.MVDataEntry; import data.io.AbstractMVDataReader; /** * Stream-oriented reader from a particular LDAP connection * Always returns lines/items sorted by lexicographical ascending key * Consistent even if there is a Writer on same LDAP connection (useful for sync) * * @author lpouzenc */ public class LDAPFlatDataReader extends AbstractMVDataReader { private final LDAPConnection conn; private final String baseDN; private final String keyAttr; private final int lookAheadAmount; private final SortedSet keys; private transient Iterator keysItCached; private transient Iterator keysItConsumed; private transient SortedMap entries; // Listener to feed LDAP search result in SortedMap without instantiating a big fat SearchResult private final SearchResultListener keysReqListener = new SearchResultListener() { private static final long serialVersionUID = 3364745402521913458L; @Override public void searchEntryReturned(SearchResultEntry searchEntry) { keys.add(searchEntry.getAttributeValue(keyAttr)); } @Override public void searchReferenceReturned(SearchResultReference searchReference) { throw new RuntimeException("Unsupported : search request for all '" + keyAttr + "' has returned at least one reference (excepected : an entry)"); } }; /** * Construct a new reader that wrap a particular LDAP search on a given connection * @param dataSourceName Short name of this data source (for logging) * @param conn Already initialized LDAP connection where run the search * @param baseDN Search base DN (will return childs of this DN) * @param keyAttr Attribute name that is the primary key of the entry, identifying the entry in a unique manner * @param lookAheadAmount Grab this amount of entries at once (in memory-sorted, 128 could be great) * @throws LDAPException */ public LDAPFlatDataReader(String dataSourceName, LDAPConnection conn, String baseDN, String keyAttr, int lookAheadAmount) throws LDAPException { this.dataSourceName = dataSourceName; this.conn = conn; this.baseDN = baseDN; this.keyAttr = keyAttr; this.lookAheadAmount = lookAheadAmount; // Grab all the entries' keys from LDAP connection and put them in this.keys this.keys = new TreeSet(); SearchRequest keysReq = new SearchRequest(keysReqListener, baseDN, SearchScope.ONE, Filter.create("(objectClass=*)"), keyAttr); conn.search(keysReq); } /** * {@inheritDoc} * Note : multiple iterators on the same instance are not supported */ @Override public Iterator iterator() { // Reset the search (it uses two different iterators on the same set) keysItCached = keys.iterator(); keysItConsumed = keys.iterator(); entries = new TreeMap(); return this; } /** * {@inheritDoc} */ @Override public boolean hasNext() { return (keysItConsumed==null)?false:keysItConsumed.hasNext(); } /** * {@inheritDoc} */ @Override public MVDataEntry next() { String wantedKey = keysItConsumed.next(); // Feed the lookAhead buffer if it is empty (and there is more elements to grab) if ( entries.isEmpty() && keysItCached.hasNext() ) { lookAhead(lookAheadAmount); } //FIXME : it is possible to have inconsistency between "entries" content and keysIt* values if some entry is deleted since we have read all the keys // Pop an entry from the lookAhead buffer MVDataEntry wantedEntry = entries.remove(wantedKey); if ( wantedEntry == null ) { throw new NoSuchElementException(); } return wantedEntry; } /** * Performs look-ahead of amount entries, using the next sorted keys previously queried. * @param amount */ private void lookAhead(int amount) { if ( amount < 1 ) { throw new IllegalArgumentException("LookAhead amount has to be >= 1"); } try { // Build a search that matches "amount" next entries Filter filter = Filter.createEqualityFilter(keyAttr, keysItCached.next()); for (int i=0; ( i < amount-1 ) && keysItCached.hasNext(); i++) { filter = Filter.createORFilter(filter, Filter.createEqualityFilter(keyAttr, keysItCached.next())); } SearchRequest searchRequest = new SearchRequest(baseDN, SearchScope.ONE, filter, "*"); // XXX Could use a second listener, as for the keys // Get all this entries in memory, convert them in MVDataEntry beans and store them in a SortedMap SearchResult search = conn.search(searchRequest); for (SearchResultEntry ldapEntry: search.getSearchEntries()) { String key = ldapEntry.getAttributeValue(keyAttr); MVDataEntry mvEntry = new MVDataEntry(key); for ( Attribute attr : ldapEntry.getAttributes() ) { mvEntry.put(attr.getName(), attr.getValues()); } entries.put(key, mvEntry); } } catch (LDAPException e) { throw new RuntimeException(e); } } }