/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de)
//
// ProjectForge is dual-licensed.
//
// This community edition 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; version 3 of the License.
//
// This community edition 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 this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////
package org.projectforge.ldap;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.commons.lang.StringUtils;
import org.projectforge.common.StringHelper;
import org.projectforge.core.Configuration;
/**
* @author Kai Reinhard (k.reinhard@micromata.de)
*/
public abstract class LdapDao<I extends Serializable, T extends LdapObject<I>>
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LdapDao.class);
protected LdapConnector ldapConnector;
protected LdapConfig ldapConfig;
protected abstract String getObjectClass();
protected abstract String[] getAdditionalObjectClasses();
protected String[] getAdditionalObjectClasses(final T obj)
{
return getAdditionalObjectClasses();
}
public abstract String getIdAttrId();
public abstract I getId(T obj);
public void create(final String ouBase, final T obj, final Object... args)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
create(ctx, ouBase, obj, args);
return null;
}
}.excecute();
}
/**
* @param ctx
* @param ouBase If organizational units are given by the given obj then this parameter will be ignored, otherwise this is the ou where
* the new object will be inserted.
* @param obj
* @param args
* @throws NamingException
*/
public void create(final DirContext ctx, final String ouBase, final T obj, final Object... args) throws NamingException
{
final String dn = buildDn(ouBase, obj);
log.info("Create " + getObjectClass() + ": " + dn + ": " + getLogInfo(obj));
final Attributes attrs = new BasicAttributes();
final List<ModificationItem> modificationItems = getModificationItems(new ArrayList<ModificationItem>(), obj);
modificationItems.add(createModificationItem(DirContext.ADD_ATTRIBUTE, "objectClass", getObjectClass()));
final String[] additionalObjectClasses = getAdditionalObjectClasses(obj);
if (additionalObjectClasses != null) {
for (final String objectClass : additionalObjectClasses) {
modificationItems.add(createModificationItem(DirContext.ADD_ATTRIBUTE, "objectClass", objectClass));
}
}
for (final ModificationItem modItem : modificationItems) {
final Attribute attr = modItem.getAttribute();
LdapUtils.putAttribute(attrs, attr.getID(), (String) attr.get());
}
LdapUtils.putAttribute(attrs, "cn", LdapUtils.escapeCommonName(obj.getCommonName()));
onBeforeBind(dn, attrs, args);
ctx.bind(dn, null, attrs);
}
protected void onBeforeBind(final String dn, final Attributes attrs, final Object... args)
{
// Do nothing at default.
}
/**
* Please do not use this method for bulk updates, use {@link #createOrUpdate(Set, Object, Object...)} instead! Calls
* {@link #getSetOfAllObjects()} before creation or update.
* @param obj
* @see #createOrUpdate(Set, Object, Object...)
*/
public void createOrUpdate(final String ouBase, final T obj, final Object... args)
{
createOrUpdate(getSetOfAllObjects(ouBase), ouBase, obj, args);
}
/**
* Please do not use this method for bulk updates, use {@link #createOrUpdate(Set, Object, Object...)} instead! Calls
* {@link #getSetOfAllObjects()} before creation or update.
* @param obj
* @throws NamingException
* @see #createOrUpdate(Set, Object, Object...)
*/
public void createOrUpdate(final DirContext ctx, final String ouBase, final T obj, final Object... args) throws NamingException
{
createOrUpdate(ctx, getSetOfAllObjects(ctx, ouBase), ouBase, obj, args);
}
/**
* Calls {@link #create(Object)} if the object isn't part of the given set, otherwise {@link #update(Object)}.
* @param setOfAllLdapObjects List generated before via {@link #getSetOfAllObjects()}.
* @param obj
*/
public void createOrUpdate(final SetOfAllLdapObjects setOfAllLdapObjects, final String ouBase, final T obj, final Object... args)
{
if (setOfAllLdapObjects.contains(obj, buildDn(ouBase, obj)) == true) {
update(ouBase, obj, args);
} else {
create(ouBase, obj, args);
}
}
/**
* Calls {@link #create(Object)} if the object isn't part of the given set, otherwise {@link #update(Object)}.
* @param setOfAllLdapObjects List generated before via {@link #getSetOfAllObjects()}.
* @param obj
* @throws NamingException
*/
public void createOrUpdate(final DirContext ctx, final SetOfAllLdapObjects setOfAllLdapObjects, final String ouBase, final T obj,
final Object... args) throws NamingException
{
if (setOfAllLdapObjects.contains(obj, buildDn(ouBase, obj)) == true) {
update(ctx, ouBase, obj, args);
} else {
create(ctx, ouBase, obj, args);
}
}
public void update(final String ouBase, final T obj, final Object... objs)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
update(ctx, ouBase, obj, objs);
return null;
}
}.excecute();
}
public void update(final DirContext ctx, final String ouBase, final T obj, final Object... objs) throws NamingException
{
modify(ctx, obj, getModificationItems(new ArrayList<ModificationItem>(), obj));
}
protected abstract List<ModificationItem> getModificationItems(final List<ModificationItem> list, final T obj);
/**
* Helper method.
* @param attrId
* @param attrValue
* @return
*/
protected ModificationItem createModificationItem(final String attrId, final String attrValue)
{
return createModificationItem(DirContext.REPLACE_ATTRIBUTE, attrId, attrValue);
}
/**
* Helper method.
* @param {@link DirContext#REPLACE_ATTRIBUTE}, {@link DirContext#ADD_ATTRIBUTE} or {@link DirContext#REMOVE_ATTRIBUTE}.
* @param attrId
* @param attrValue
* @return
*/
protected ModificationItem createModificationItem(final int mode, final String attrId, final String attrValue)
{
return new ModificationItem(mode, new BasicAttribute(attrId, attrValue));
}
/**
* Helper method for appending modification item(s) to a given list. At least one entry will be added if no attrValue is given.
* @param list
* @param attrId
* @param attrValues If null then a null-value will be assumed. If more than one string is given, multiple modification items will be
* added.
* @return
*/
protected void createAndAddModificationItems(final List<ModificationItem> list, final String attrId, final String... attrValues)
{
if (attrValues == null) {
list.add(createModificationItem(attrId, null));
return;
}
boolean added = false;
for (final String attrValue : attrValues) {
if (StringUtils.isEmpty(attrValue) == true && added == true) {
continue;
}
final String val = StringUtils.isEmpty(attrValue) == true ? null : attrValue;
if (added == false) {
list.add(createModificationItem(DirContext.REPLACE_ATTRIBUTE, attrId, val));
added = true;
} else {
list.add(createModificationItem(DirContext.ADD_ATTRIBUTE, attrId, val));
}
}
}
/**
* Helper method for appending modification item(s) to a given list. At least one entry will be added if no attrValue is given.
* @param list
* @param attrId
* @param attrValues If null then a null-value will be assumed. If more than one string is given, multiple modification items will be
* added.
* @return
*/
protected void createAndAddModificationItems(final List<ModificationItem> list, final String attrId, final Set<String> attrValues)
{
if (attrValues == null) {
list.add(createModificationItem(attrId, null));
return;
}
boolean added = false;
for (final String attrValue : attrValues) {
if (StringUtils.isEmpty(attrValue) == true && added == true) {
continue;
}
final String val = StringUtils.isEmpty(attrValue) == true ? null : attrValue;
if (added == false) {
list.add(createModificationItem(DirContext.REPLACE_ATTRIBUTE, attrId, val));
added = true;
} else {
list.add(createModificationItem(DirContext.ADD_ATTRIBUTE, attrId, val));
}
}
}
public void modify(final T obj, final List<ModificationItem> modificationItems)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
modify(ctx, obj, modificationItems);
return null;
}
}.excecute();
}
public void modify(final DirContext ctx, final T obj, final List<ModificationItem> modificationItems) throws NamingException
{
final Object id = getId(obj);
// The dn is may-be changed, so find the original dn by id:
final T origObject = findById(ctx, id, obj.getOrganizationalUnit());
if (origObject == null) {
throw new RuntimeException("Object with id "
+ id
+ " not found in search base '"
+ StringHelper.listToString(",", obj.getOrganizationalUnit())
+ "'. Can't modify the object: "
+ obj);
}
final String dn = origObject.getDn();
log.info("Modify attributes of " + getObjectClass() + ": " + dn + ": " + getLogInfo(obj));
final ModificationItem[] items = modificationItems.toArray(new ModificationItem[modificationItems.size()]);
ctx.modifyAttributes(dn, items);
// Don't move object.
// if (obj.getDn() != null && StringUtils.equals(dn, obj.getDn()) == false) {
// log.info("DN of object is changed from '" + dn + "' to '" + obj.getDn());
// ctx.rename(dn, obj.getDn());
// }
}
public void move(final T obj, final String newOrganizationalUnit)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
move(ctx, obj, newOrganizationalUnit);
return null;
}
}.excecute();
}
public void move(final DirContext ctx, final T obj, final String newOrganizationalUnit) throws NamingException
{
final Object id = getId(obj);
// The dn is may-be changed, so find the original dn by id:
final T origObject = findById(id, obj.getOrganizationalUnit());
if (origObject == null) {
throw new RuntimeException("Object with id "
+ id
+ " not found in search base '"
+ StringHelper.listToString(",", obj.getOrganizationalUnit())
+ "'. Can't move the object: "
+ obj);
}
final String ou = LdapUtils.getOrganizationalUnit(newOrganizationalUnit);
final String origOu = LdapUtils.getOu(origObject.getOrganizationalUnit());
if (StringUtils.equals(origOu, ou) == false) {
log.info("Move object with id '" + obj.getId() + "' from '" + origOu + "' to '" + ou);
final String dnIdentifier = buildDnIdentifier(obj);
ctx.rename(dnIdentifier + "," + origOu, dnIdentifier + "," + ou);
}
}
public void rename(final DirContext ctx, final T obj, final T oldObj) throws NamingException
{
final String newDnIdentifier = buildDnIdentifier(obj);
final String oldDnIdentifier = buildDnIdentifier(oldObj);
if (StringUtils.equals(newDnIdentifier, oldDnIdentifier) == true) {
// Nothing to rename.
return;
}
final Object id = getId(obj);
// The dn is may-be changed, so find the original dn by id:
final T origObject = findById(id, obj.getOrganizationalUnit());
if (origObject == null) {
throw new RuntimeException("Object with id "
+ id
+ " not found in search base '"
+ StringHelper.listToString(",", obj.getOrganizationalUnit())
+ "'. Can't rename the object: "
+ obj);
}
final String ou = LdapUtils.getOu(origObject.getOrganizationalUnit());
log.info("Rename object with id '" + obj.getId() + "' from '" + oldDnIdentifier + "' to '" + newDnIdentifier);
ctx.rename(oldDnIdentifier + "," + ou, newDnIdentifier + "," + ou);
}
protected String getLogInfo(final T obj)
{
return String.valueOf(obj);
}
protected void onBeforeRebind(final String dn, final Attributes attrs, final Object... objs)
{
// Do nothing at default;
}
public void delete(final T obj)
{
new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
delete(ctx, obj);
return null;
}
}.excecute();
}
public void delete(final DirContext ctx, final T obj) throws NamingException
{
final String dn = buildDn(null, obj);
log.info("Delete " + getObjectClass() + ": " + dn + ": " + getLogInfo(obj));
ctx.unbind(dn);
}
@SuppressWarnings("unchecked")
public List<T> findAll(final String organizationalUnit)
{
return (List<T>) new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
return findAll(ctx, organizationalUnit);
}
}.excecute();
}
public List<T> findAll(final DirContext ctx, final String organizationalUnit) throws NamingException
{
final LinkedList<T> list = new LinkedList<T>();
NamingEnumeration< ? > results = null;
final SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
final String searchBase = getSearchBase(organizationalUnit);
results = ctx.search(searchBase, "(objectclass=" + getObjectClass() + ")", controls);
while (results.hasMore()) {
final SearchResult searchResult = (SearchResult) results.next();
final String dn = searchResult.getName();
final Attributes attributes = searchResult.getAttributes();
list.add(mapToObject(dn, searchBase, attributes));
}
return list;
}
@SuppressWarnings("unchecked")
public T findById(final Object id, final String... organizationalUnits)
{
return (T) new LdapTemplate(ldapConnector) {
@Override
protected Object call() throws NameNotFoundException, Exception
{
return findById(ctx, id, organizationalUnits);
}
}.excecute();
}
public T findById(final DirContext ctx, final Object id, final String... organizationalUnits) throws NamingException
{
NamingEnumeration< ? > results = null;
final SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
final String searchBase = getSearchBase(organizationalUnits);
final String args = "(&(objectClass=" + getObjectClass() + ")(" + getIdAttrId() + "=" + buildId(id) + "))";
results = ctx.search(searchBase, args, controls);
if (results.hasMore() == false) {
return null;
}
final SearchResult searchResult = (SearchResult) results.next();
final String dn = searchResult.getName();
final Attributes attributes = searchResult.getAttributes();
if (results.hasMore() == true) {
log.error("Oups, found entries with multiple id's: " + getObjectClass() + "." + id);
}
return mapToObject(dn, searchBase, attributes);
}
/**
* The given id is modified if the id in id-attr is stored e. g. with a prefix. See implementation of {@link LdapUserDao#buildId(Object)}
* as an example.
* @param id
*/
protected String buildId(final Object id)
{
return String.valueOf(id);
}
/**
* Set of all objects (the string is built from the method {@link #buildDn(Object)}).
*/
public SetOfAllLdapObjects getSetOfAllObjects(final String organizationalUnit)
{
final SetOfAllLdapObjects set = new SetOfAllLdapObjects();
final List<T> all = findAll(organizationalUnit);
for (final T obj : all) {
if (log.isDebugEnabled() == true) {
log.debug("Adding: " + obj.getDn());
}
set.add(obj);
}
return set;
}
/**
* Set of all objects (the string is built from the method {@link #buildDn(Object)}).
* @throws NamingException
*/
public SetOfAllLdapObjects getSetOfAllObjects(final DirContext ctx, final String organizationalUnit) throws NamingException
{
final SetOfAllLdapObjects set = new SetOfAllLdapObjects();
final List<T> all = findAll(ctx, organizationalUnit);
for (final T obj : all) {
if (log.isDebugEnabled() == true) {
log.debug("Adding: " + obj.getDn());
}
set.add(obj);
}
return set;
}
/**
* At default the identifier in dn is cn (cn=xxx,ou=yyy,ou=zzz). But for users the dn is may-be (uid=xxx,ou=yyy,ou=zzz).
* @param obj
* @return
*/
protected String buildDnIdentifier(final T obj)
{
return "cn=" + LdapUtils.escapeCommonName(obj.getCommonName());
}
/**
* Sets dn of object and organizationalUnit if not already given.
* @param ouBase If {@link T#getOrganizationalUnit()} is not given, ouBase is used for building dn, otherwise ouBase is ignored.
* @param obj
* @return
*/
protected String buildDn(final String ouBase, final T obj)
{
final StringBuffer buf = new StringBuffer();
buf.append(buildDnIdentifier(obj));
if (obj.getOrganizationalUnit() != null) {
buf.append(',');
LdapUtils.buildOu(buf, obj.getOrganizationalUnit());
} else if (ouBase != null) {
buf.append(',');
LdapUtils.buildOu(buf, ouBase);
obj.setOrganizationalUnit(ouBase);
}
obj.setDn(buf.toString());
return obj.getDn();
}
protected T mapToObject(final String dn, final String ouBase, final Attributes attributes) throws NamingException
{
String fullDn;
if (StringUtils.isNotBlank(ouBase) == true) {
fullDn = dn + "," + ouBase;
} else {
fullDn = dn;
}
final T obj = mapToObject(fullDn, attributes);
obj.setDn(fullDn);
obj.setOrganizationalUnit(LdapUtils.getOrganizationalUnit(dn, ouBase));
obj.setCommonName(LdapUtils.getAttributeStringValue(attributes, "cn"));
obj.setObjectClasses(LdapUtils.getAttributeStringValues(attributes, "objectClass"));
return obj;
}
/**
*
* @param dn
* @param attributes
* @return
* @throws NamingException
*/
protected abstract T mapToObject(final String dn, final Attributes attributes) throws NamingException;
/**
* Used by {@link #findById(DirContext, Object, String...)} etc. for setting search-base if not given.
* @return
*/
protected abstract String getOuBase();
protected String getSearchBase(final String... organizationalUnits)
{
String searchBase = LdapUtils.getOu(organizationalUnits);
if (StringUtils.isBlank(searchBase) == true) {
searchBase = getOuBase();
if (StringUtils.isBlank(searchBase) == true) {
if (Configuration.isDevelopmentMode() == true) {
throw new RuntimeException("No ou given!");
} else {
log.warn("Oups, no search-base (ou) given. Searching in whole LDAP tree!");
}
}
}
return searchBase;
}
public LdapDao<I, T> setLdapConnector(final LdapConnector ldapConnector)
{
this.ldapConnector = ldapConnector;
this.ldapConfig = ldapConnector.getLdapConfig();
return this;
}
}