/** * Licensed to the Austrian Association for Software Tool Integration (AASTI) * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. The AASTI licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.openengsb.infrastructure.ldap; import java.util.LinkedList; import java.util.List; import org.apache.directory.ldap.client.api.DefaultSchemaLoader; import org.apache.directory.ldap.client.api.LdapConnection; import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.directory.shared.ldap.model.cursor.EntryCursor; import org.apache.directory.shared.ldap.model.cursor.SearchCursor; import org.apache.directory.shared.ldap.model.entry.Entry; import org.apache.directory.shared.ldap.model.exception.LdapException; import org.apache.directory.shared.ldap.model.message.AddRequest; import org.apache.directory.shared.ldap.model.message.AddRequestImpl; import org.apache.directory.shared.ldap.model.message.BindRequest; import org.apache.directory.shared.ldap.model.message.BindRequestImpl; import org.apache.directory.shared.ldap.model.message.DeleteRequest; import org.apache.directory.shared.ldap.model.message.DeleteRequestImpl; import org.apache.directory.shared.ldap.model.message.LdapResult; import org.apache.directory.shared.ldap.model.message.ResultCodeEnum; import org.apache.directory.shared.ldap.model.message.SearchRequest; import org.apache.directory.shared.ldap.model.message.SearchRequestImpl; import org.apache.directory.shared.ldap.model.message.SearchScope; import org.apache.directory.shared.ldap.model.name.Dn; import org.openengsb.infrastructure.ldap.model.Node; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Dao which provides CRUD methods for Ldap {@link Entry}s. * */ public class LdapDao { private static final Logger LOGGER = LoggerFactory.getLogger(LdapDao.class); private LdapConnection connection; /** * Creates a new LdapDao. Its connection will use the given parameters. * */ public LdapDao(String host, int port) { this.connection = new LdapNetworkConnection(host, port); } /** * Constructs a new LdapDao communicating with an Ldap server over given connection. * */ public LdapDao(LdapConnection connection) { this.connection = connection; } public LdapDao() { } /** * Sets the {@link LdapConnection} used by this dao to communicate with an Ldap server. * */ public void setConnection(LdapConnection connection) { this.connection = connection; } /** * Returns the {@link LdapConnection} used by this dao to communicate with an Ldap server. * */ public LdapConnection getConnection() { return connection; } /** * Opens the dao's connection. Dn and credentials are used for authentication. Throws an {@link LdapDaoException} in * case of IO error or invalid dn. * */ public void connect(String dn, String credentials) { BindRequest bindRequest = new BindRequestImpl(); bindRequest.setCredentials(credentials); try { bindRequest.setDn(new Dn(dn)); connection.connect(); connection.bind(bindRequest); ((LdapNetworkConnection) connection).loadSchema(new DefaultSchemaLoader(connection)); } catch (Exception e) { throw new LdapDaoException(e); } } /** * Closes the dao's connection. {@link LdapDaoException} is thrown in case of IO error. * */ public void disconnect() { try { connection.unBind(); connection.close(); } catch (Exception e) { throw new LdapDaoException(e); } } /** * Inserts an entry into the DIT. Throws {@link EntryAlreadyExistsException} if an entry with given Dn already * exists. Throws {@link MissingParentException} if an ancestor of the entry is missing. */ public void store(Entry entry) throws EntryAlreadyExistsException, MissingParentException { AddRequest addRequest = new AddRequestImpl().setEntry(entry); LdapResult result; try { result = connection.add(addRequest).getLdapResult(); } catch (LdapException e) { throw new LdapDaoException(e); } if (result.getResultCode() == ResultCodeEnum.ENTRY_ALREADY_EXISTS) { throw new EntryAlreadyExistsException(entry); } else if (result.getResultCode() == ResultCodeEnum.NO_SUCH_OBJECT) { throw new MissingParentException(lastMatch(entry.getDn())); } else if (result.getResultCode() != ResultCodeEnum.SUCCESS) { throw new LdapDaoException(result.getDiagnosticMessage()); } } /** * Inserts an entry into the DIT. If the entry exists, nothing is done. Throws {@link MissingParentException} if an * ancestor of the entry is missing. */ public void storeSkipExisting(Entry entry) throws MissingParentException { try { store(entry); } catch (EntryAlreadyExistsException e) { LOGGER.debug("Entry already exists. Skipping."); } } /** * Inserts an entry into the DIT. If the entry exists, it is overwritten. Overwriting leaves is straight forward. * Overwriting inner nodes will first delete the node's entire subtree, then overwrite the node. Throws * {@link MissingParentException} if an ancestor of the entry is missing. */ public void storeOverwriteExisting(Entry entry) throws MissingParentException { try { deleteSubtreeIncludingRoot(entry.getDn()); } catch (NoSuchNodeException e) { LOGGER.debug("nothing to overwrite"); } finally { try { store(entry); } catch (EntryAlreadyExistsException e) { LOGGER.warn("should never reach here - entry should have been deleted"); } } } /** * Inserts a hierarchy (tree) of entries into the DIT. The hierarchy is passed as a list. The order of the entries * in the list is important. Throws {@link MissingParentException} if an ancestor of some entry is missing. In * particular this will happen if the list is not ordered properly. If the root entry (=first) already exists, an * {@link EntryAlreadyExistsException} is thrown. Note that this action is not atomic. */ public void store(List<Entry> entries) throws EntryAlreadyExistsException, MissingParentException { for (Entry e : entries) { store(e); } } /** * Behaves like {@link #store(List)} except that duplicates do not throw an exception but are collected in a list * which will be returned at success. Note that this action is not atomic. */ public List<Entry> storeSkipExisting(List<Entry> entries) throws MissingParentException { List<Entry> skippedEntries = new LinkedList<Entry>(); for (Entry e : entries) { try { store(e); } catch (EntryAlreadyExistsException ex) { skippedEntries.add(e); } } return skippedEntries; } /** * Behaves like {@link #store(List)} except that duplicates do not throw an exception but are overwritten as * described in {@link #storeOverwriteExisting(Entry)}. Note that this action is not atomic. * */ public void storeOverwriteExisting(List<Entry> entries) throws NoSuchNodeException, MissingParentException { for (Entry entry : entries) { try { store(entry); } catch (EntryAlreadyExistsException e) { deleteSubtreeIncludingRoot(e.getEntry().getDn()); try { store(entry); } catch (EntryAlreadyExistsException e1) { LOGGER.debug("should never reach here"); throw new LdapDaoException(e1); } } } } /** * Returns all entries found exactly one level below parent. Throws {@link NoSuchNodeException} if resolving the * argument Dn fails at its leaf. Throws {@link MissingParentException} if resolving fails earlier. * */ public List<Entry> getDirectChildren(Dn parent) throws NoSuchNodeException, MissingParentException { return extractEntriesFromCursor(searchOneLevel(parent)); } /** * Returns a list of {@link Node}s representing the subtrees of the argument Dn. * */ public List<Node> searchSubtree(Dn parent) throws NoSuchNodeException, MissingParentException { SearchCursor cursor = searchOneLevel(parent); return extractNodesFromCursor(cursor); } /** * Deletes root and its entire subtree. Throws NoSuchNodeException if root does not exist. Throws * MissingParentException if some node above root does not exist. * */ public void deleteSubtreeIncludingRoot(Dn root) throws NoSuchNodeException, MissingParentException { deleteSubtreeExcludingRoot(root); deleteLeaf(root); } /** * Deletes the entire subtree of root but not root itself. Throws NoSuchNodeException if root does not exist. Throws * MissingParentException if some node above root does not exist. * */ public void deleteSubtreeExcludingRoot(Dn root) throws NoSuchNodeException, MissingParentException { existsCheck(root); try { EntryCursor entryCursor = connection.search(root, "(objectclass=*)", SearchScope.ONELEVEL); while (entryCursor.next()) { deleteSubtreeIncludingRoot(entryCursor.get().getDn()); } } catch (Exception e) { throw new LdapDaoException(e); } } /** * Returns true if dn exists, false otherwise. * */ public boolean exists(Dn dn) { try { return connection.exists(dn); } catch (LdapException e) { throw new LdapDaoException(e); } } /** * Returns the {@link Entry} with given {@link Dn}. If dn does not exist but its parent does, a * {@link NoSuchNodeException} is thrown. If one of dn's ancestors does not exist, a {@link MissingParentException} * is thrown. */ public Entry lookup(Dn dn) throws NoSuchNodeException, MissingParentException { existsCheck(dn); try { return connection.lookup(dn); } catch (LdapException e) { throw new LdapDaoException(e); } } /** * Throws appropriate exceptions for connection.exists(dn). * */ private void existsCheck(Dn dn) throws NoSuchNodeException, MissingParentException { try { if (!connection.exists(dn.getParent())) { throw new MissingParentException(lastMatch(dn)); } else if (!connection.exists(dn)) { throw new NoSuchNodeException(dn); } } catch (LdapException e) { throw new LdapDaoException(e); } } /** * Returns a SearchCursor over the direct children of Dn parent. Throws {@link NoSuchNodeException} if resolving the * argument Dn fails at its leaf. Throws {@link MissingParentException} if resolving fails earlier. * */ private SearchCursor searchOneLevel(Dn parent) throws NoSuchNodeException, MissingParentException { try { if (!connection.exists(parent.getParent())) { throw new MissingParentException(lastMatch(parent)); } else if (!connection.exists(parent)) { throw new NoSuchNodeException(parent); } } catch (LdapException e) { throw new LdapDaoException(e); } SearchRequest searchRequest = new SearchRequestImpl(); searchRequest.setBase(parent); searchRequest.setScope(SearchScope.ONELEVEL); try { searchRequest.setFilter("(objectclass=*)"); return connection.search(searchRequest); } catch (LdapException e) { throw new LdapDaoException(e); } } private List<Entry> extractEntriesFromCursor(SearchCursor cursor) { List<Entry> result = new LinkedList<Entry>(); try { while (cursor.next()) { result.add(cursor.getEntry()); } } catch (Exception e) { throw new LdapDaoException(e); } return result; } private List<Node> extractNodesFromCursor(SearchCursor cursor) { LinkedList<Node> result = new LinkedList<Node>(); try { while (cursor.next()) { Node node = new Node(cursor.getEntry()); result.addFirst(node); node.setChildren(searchSubtree(node.getEntry().getDn())); for (Node n : node.getChildren()) { n.setParent(node); } } } catch (Exception e) { throw new LdapDaoException(e); } return result; } private void deleteLeaf(Dn dn) { DeleteRequest deleteRequest = new DeleteRequestImpl(); deleteRequest.setName(dn); LdapResult result; try { result = connection.delete(deleteRequest).getLdapResult(); } catch (LdapException e) { throw new LdapDaoException(e); } if (result.getResultCode() != ResultCodeEnum.SUCCESS) { throw new LdapDaoException(result.getDiagnosticMessage()); } } private Dn lastMatch(final Dn dn) throws MissingParentException { if (dn == null) { throw new MissingParentException((Dn) null); } try { if (connection.exists(dn)) { return dn; } else { return lastMatch(dn.getParent()); } } catch (LdapException e) { throw new LdapDaoException(e); } } }