summaryrefslogtreecommitdiff
path: root/src/connectors/src/data/io/ldap
diff options
context:
space:
mode:
Diffstat (limited to 'src/connectors/src/data/io/ldap')
-rw-r--r--src/connectors/src/data/io/ldap/LDAPConnectionWrapper.java112
-rw-r--r--src/connectors/src/data/io/ldap/LDAPFlatDataReader.java178
-rw-r--r--src/connectors/src/data/io/ldap/LDAPFlatDataWriter.java198
3 files changed, 488 insertions, 0 deletions
diff --git a/src/connectors/src/data/io/ldap/LDAPConnectionWrapper.java b/src/connectors/src/data/io/ldap/LDAPConnectionWrapper.java
new file mode 100644
index 0000000..3f6497b
--- /dev/null
+++ b/src/connectors/src/data/io/ldap/LDAPConnectionWrapper.java
@@ -0,0 +1,112 @@
+/*
+ * SSSync, a Simple and Stupid Synchronizer for data with multi-valued attributes
+ * Copyright (C) 2014 Ludovic Pouzenc <ludovic@pouzenc.fr>
+ *
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+package data.io.ldap;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPConnectionOptions;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.ResultCode;
+
+/**
+ * TODO javadoc
+ *
+ * @author lpouzenc
+ */
+public class LDAPConnectionWrapper implements Closeable {
+
+ private final LDAPConnection conn;
+
+ /**
+ * TODO javadoc
+ * @param host
+ * @param port
+ * @param bindDN
+ * @param password
+ */
+ public LDAPConnectionWrapper(String host, int port, String bindDN, String password) {
+ LDAPConnectionOptions options = new LDAPConnectionOptions();
+ options.setAbandonOnTimeout(true);
+ options.setAllowConcurrentSocketFactoryUse(true);
+ options.setAutoReconnect(true);
+ options.setCaptureConnectStackTrace(true);
+ options.setConnectTimeoutMillis(2000); // 2 seconds
+ options.setResponseTimeoutMillis(5000); // 5 seconds
+ options.setUseSynchronousMode(false);
+
+ BindResult bindResult=null;
+ try {
+ conn = new LDAPConnection(options, host, port);
+ bindResult = conn.bind(bindDN, password);
+ }
+ catch (LDAPException e) {
+ throw new RuntimeException(e);
+ }
+
+ ResultCode resultCode = bindResult.getResultCode();
+ if ( resultCode != ResultCode.SUCCESS ) {
+ throw new RuntimeException("LDAP Bind failed : " + resultCode);
+ }
+ }
+
+ /**
+ * Builds a new reader against current connection and a LDAP baseDN.
+ *
+ * @param dataSourceName Short name of this data source (for logging)
+ * @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)
+ * @return A new reader ready to iterate on search results
+ */
+ public LDAPFlatDataReader newFlatReader(String dataSourceName, String baseDN, String keyAttr, int lookAheadAmount) {
+ try {
+ return new LDAPFlatDataReader(dataSourceName, conn, baseDN, keyAttr, lookAheadAmount);
+ } catch (LDAPException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Builds a new writer that could insert/update/delete entries on a particular LDAP connection and baseDN.
+ *
+ * @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
+ * @return A new writter limited on a particular baseDN
+ */
+ public LDAPFlatDataWriter newFlatWriter(String baseDN, String keyAttr) {
+ try {
+ return new LDAPFlatDataWriter(conn, baseDN, keyAttr);
+ } catch (LDAPException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Close the current ldap connection.
+ */
+ @Override
+ public void close() throws IOException {
+ this.conn.close();
+ }
+}
diff --git a/src/connectors/src/data/io/ldap/LDAPFlatDataReader.java b/src/connectors/src/data/io/ldap/LDAPFlatDataReader.java
new file mode 100644
index 0000000..2cc79a8
--- /dev/null
+++ b/src/connectors/src/data/io/ldap/LDAPFlatDataReader.java
@@ -0,0 +1,178 @@
+/*
+ * SSSync, a Simple and Stupid Synchronizer for data with multi-valued attributes
+ * Copyright (C) 2014 Ludovic Pouzenc <ludovic@pouzenc.fr>
+ *
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+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<String> keys;
+
+ private transient Iterator<String> keysItCached;
+ private transient Iterator<String> keysItConsumed;
+ private transient SortedMap<String, MVDataEntry> 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<String>();
+ 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<MVDataEntry> iterator() {
+ // Reset the search (it uses two different iterators on the same set)
+ keysItCached = keys.iterator();
+ keysItConsumed = keys.iterator();
+ entries = new TreeMap<String, MVDataEntry>();
+
+ 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);
+ }
+ }
+}
diff --git a/src/connectors/src/data/io/ldap/LDAPFlatDataWriter.java b/src/connectors/src/data/io/ldap/LDAPFlatDataWriter.java
new file mode 100644
index 0000000..d1b8918
--- /dev/null
+++ b/src/connectors/src/data/io/ldap/LDAPFlatDataWriter.java
@@ -0,0 +1,198 @@
+/*
+ * SSSync, a Simple and Stupid Synchronizer for data with multi-valued attributes
+ * Copyright (C) 2014 Ludovic Pouzenc <ludovic@pouzenc.fr>
+ *
+ * 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 <http://www.gnu.org/licenses/>
+ */
+
+package data.io.ldap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.DN;
+import com.unboundid.ldap.sdk.DeleteRequest;
+import com.unboundid.ldap.sdk.Entry;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.Modification;
+import com.unboundid.ldap.sdk.ModificationType;
+import com.unboundid.ldap.sdk.ModifyRequest;
+import com.unboundid.ldap.sdk.RDN;
+import com.unboundid.ldap.sdk.schema.EntryValidator;
+import com.unboundid.ldif.LDIFException;
+
+import data.MVDataEntry;
+import data.io.AbstractMVDataWriter;
+
+/**
+ * Stream-oriented LDAP writer from a particular LDAP Directory connection.
+ *
+ * @author lpouzenc
+ */
+public class LDAPFlatDataWriter extends AbstractMVDataWriter {
+
+ private final LDAPConnection conn;
+ private final DN baseDN;
+ private final String keyAttr;
+ private final EntryValidator validator;
+
+ /**
+ * Construct a new writer that could insert/update/delete entries on a particular LDAP connection and baseDN.
+ *
+ * @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
+ * @throws LDAPException
+ */
+ public LDAPFlatDataWriter(LDAPConnection conn, String baseDN, String keyAttr) throws LDAPException {
+ this.conn = conn;
+ this.baseDN = new DN(baseDN);
+ this.keyAttr = keyAttr;
+ this.validator = new EntryValidator(conn.getSchema());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insert(MVDataEntry newEntry) throws LDAPException {
+ // Build the DN
+ DN dn = new DN(new RDN(keyAttr, newEntry.getKey()), baseDN);
+
+ // Convert storage objects
+ Collection<Attribute> attributes = new ArrayList<Attribute>();
+ for ( Map.Entry<String, String> entry : newEntry.getAllEntries() ) {
+ attributes.add(new Attribute(entry.getKey(), entry.getValue()));
+ }
+ Entry newLDAPEntry = new Entry(dn, attributes);
+
+ // Add the entry
+ if ( dryRun ) {
+ // In dry-run mode, validate the entry
+ ArrayList<String> invalidReasons = new ArrayList<String>(5);
+ boolean valid = validator.entryIsValid(newLDAPEntry, invalidReasons);
+ if ( !valid ) throw new RuntimeException(
+ "Entry validator has failed to verify this entry :\n" + newLDAPEntry.toLDIFString() +
+ "Reasons are :\n" + invalidReasons);
+ } else {
+ // In real-run mode, insert the entry
+ try {
+ conn.add(newLDAPEntry);
+ } catch (LDAPException e) {
+ throw new LDAPException(e.getResultCode(), "Error while inserting this entry :\n" + newLDAPEntry.toLDIFString(), e);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void update(MVDataEntry updatedEntry, MVDataEntry originalEntry, Set<String> attrToUpdate) throws LDAPException, LDIFException {
+ // Build the DN
+ DN dn = new DN(new RDN(keyAttr, updatedEntry.getKey()), baseDN);
+
+ // Convert storage objects
+ List<Modification> mods = new ArrayList<Modification>();
+ for ( String attr : attrToUpdate ) {
+ Set<String> originalValues = originalEntry.getValues(attr);
+ Set<String> updatedValues = updatedEntry.getValues(attr);
+
+ Modification modification = null;
+
+ if ( updatedValues.isEmpty() ) {
+ modification = new Modification(ModificationType.DELETE, attr);
+ } else {
+ String[] updatedValuesArr = updatedValues.toArray(new String[0]);
+
+ if ( originalValues.isEmpty() ) {
+ modification = new Modification(ModificationType.ADD, attr, updatedValuesArr);
+ } else {
+ modification = new Modification(ModificationType.REPLACE, attr, updatedValuesArr);
+ }
+ }
+
+ mods.add(modification);
+ }
+ ModifyRequest modReq = new ModifyRequest(dn, mods);
+
+ // Update the entry
+ if ( dryRun ) {
+ // Simulate originalEntry update
+ Collection<Attribute> attributes = new ArrayList<Attribute>();
+ for ( Map.Entry<String, String> entry : originalEntry.getAllEntries() ) {
+ attributes.add(new Attribute(entry.getKey(), entry.getValue()));
+ }
+ Entry originalLDAPEntry = new Entry(dn, attributes);
+
+ // Warning : Unboundid SDK is okay with mandatory attributes with value "" (empty string)
+ // OpenLDAP do not allow that empty strings in mandatory attributes.
+ // Empty strings are discarded by MVDataEntry.put() for now.
+ Entry modifiedLDAPEntry;
+ try {
+ modifiedLDAPEntry = Entry.applyModifications(originalLDAPEntry, false, mods);
+ } catch (LDAPException originalException) {
+ throw new RuntimeException("Entry update simulation has failed while running applyModifications()\n"
+ + "original entry : " + originalEntry + "\n"
+ + "wanted updated entry : " + updatedEntry + "\n"
+ + "modification request : " + modReq,
+ originalException);
+ }
+ ArrayList<String> invalidReasons = new ArrayList<String>(5);
+ boolean valid = validator.entryIsValid(modifiedLDAPEntry, invalidReasons);
+ if ( !valid ) throw new RuntimeException("Entry update simulation has failed while checking entryIsValid()\n"
+ + "modified entry : " + modifiedLDAPEntry.toLDIFString() + "\n"
+ + "reasons :" + invalidReasons);
+ } else {
+ // In real-run mode, update the entry
+ try {
+ conn.modify(modReq);
+ } catch (LDAPException originalException) {
+ throw new LDAPException(originalException.getResultCode(),
+ "Error while updating this entry :\n" + modReq.toLDIFString(),
+ originalException);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void delete(MVDataEntry existingEntry) throws LDAPException {
+ // Build the DN
+ DN dn = new DN(new RDN(keyAttr, existingEntry.getKey()), baseDN);
+
+ // Delete the entry
+ try {
+ if ( dryRun ) {
+ //XXX : try to verify the entry existence in dry-run mode ?
+ } else {
+ conn.delete(new DeleteRequest(dn));
+ }
+ } catch (LDAPException originalException) {
+ throw new LDAPException(originalException.getResultCode(),
+ "Error while deleting this dn : " + dn.toString(),
+ originalException);
+ }
+ }
+
+}