package org.keycloak.federation.ldap.idm.store.ldap;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

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.SearchResult;

import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.idm.model.AttributedType;
import org.keycloak.federation.ldap.idm.model.IdentityType;
import org.keycloak.federation.ldap.idm.model.LDAPUser;
import org.keycloak.federation.ldap.idm.query.AttributeParameter;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.IdentityQuery;
import org.keycloak.federation.ldap.idm.query.IdentityQueryBuilder;
import org.keycloak.federation.ldap.idm.query.QueryParameter;
import org.keycloak.federation.ldap.idm.query.internal.BetweenCondition;
import org.keycloak.federation.ldap.idm.query.internal.DefaultQueryBuilder;
import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
import org.keycloak.federation.ldap.idm.query.internal.GreaterThanCondition;
import org.keycloak.federation.ldap.idm.query.internal.InCondition;
import org.keycloak.federation.ldap.idm.query.internal.LessThanCondition;
import org.keycloak.federation.ldap.idm.query.internal.LikeCondition;
import org.keycloak.federation.ldap.idm.store.IdentityStore;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.utils.reflection.NamedPropertyCriteria;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyQueries;
import org.keycloak.models.utils.reflection.TypedPropertyCriteria;
import org.keycloak.util.reflections.Reflections;

/**
 * An IdentityStore implementation backed by an LDAP directory
 *
 * @author Shane Bryzak
 * @author Anil Saldhana
 * @author <a href="mailto:psilva@redhat.com">Pedro Silva</a>
 */
public class LDAPIdentityStore implements IdentityStore {

    private static final Logger logger = Logger.getLogger(LDAPIdentityStore.class);

    public static final String EMPTY_ATTRIBUTE_VALUE = " ";

    private final LDAPIdentityStoreConfiguration config;
    private final LDAPOperationManager operationManager;

    public LDAPIdentityStore(LDAPIdentityStoreConfiguration config) {
        this.config = config;

        try {
            this.operationManager = new LDAPOperationManager(getConfig());
        } catch (NamingException e) {
            throw new ModelException("Couldn't init operation manager", e);
        }
    }

    @Override
    public LDAPIdentityStoreConfiguration getConfig() {
        return this.config;
    }

    @Override
    public void add(AttributedType attributedType) {
        // id will be assigned by the ldap server
        attributedType.setId(null);

        String entryDN = getBindingDN(attributedType, true);
        this.operationManager.createSubContext(entryDN, extractAttributes(attributedType, true));
        addToParentAsMember(attributedType);
        attributedType.setId(getEntryIdentifier(attributedType));

        attributedType.setEntryDN(entryDN);

        if (logger.isTraceEnabled()) {
            logger.tracef("Type with identifier [%s] successfully added to identity store [%s].", attributedType.getId(), this);
        }
    }

    @Override
    public void update(AttributedType attributedType) {
        BasicAttributes updatedAttributes = extractAttributes(attributedType, false);
        NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();

        this.operationManager.modifyAttributes(getBindingDN(attributedType, true), attributes);

        if (logger.isTraceEnabled()) {
            logger.tracef("Type with identifier [%s] successfully updated to identity store [%s].", attributedType.getId(), this);
        }
    }

    @Override
    public void remove(AttributedType attributedType) {
        LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());

        this.operationManager.removeEntryById(getBaseDN(attributedType), attributedType.getId(), mappingConfig);

        if (logger.isTraceEnabled()) {
            logger.tracef("Type with identifier [%s] successfully removed from identity store [%s].", attributedType.getId(), this);
        }
    }

    @Override
    public <V extends IdentityType> List<V> fetchQueryResults(IdentityQuery<V> identityQuery) {
        List<V> results = new ArrayList<V>();

        try {
            if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
                throw new ModelException("LDAP Identity Store does not support sorted queries.");
            }

            for (Condition condition : identityQuery.getConditions()) {

                if (IdentityType.ID.equals(condition.getParameter())) {
                    if (EqualCondition.class.isInstance(condition)) {
                        EqualCondition equalCondition = (EqualCondition) condition;
                        SearchResult search = this.operationManager
                                .lookupById(getConfig().getBaseDN(), equalCondition.getValue().toString(), null);

                        if (search != null) {
                            results.add((V) populateAttributedType(search, null));
                        }
                    }

                    return results;
                }
            }

            if (!IdentityType.class.equals(identityQuery.getIdentityType())) {
                // the ldap store does not support queries based on root types. Except if based on the identifier.
                LDAPMappingConfiguration ldapEntryConfig = getMappingConfig(identityQuery.getIdentityType());
                StringBuilder filter = createIdentityTypeSearchFilter(identityQuery, ldapEntryConfig);
                String baseDN = getBaseDN(ldapEntryConfig);
                List<SearchResult> search;

                if (getConfig().isPagination() && identityQuery.getLimit() > 0) {
                    search = this.operationManager.searchPaginated(baseDN, filter.toString(), ldapEntryConfig, identityQuery);
                } else {
                    search = this.operationManager.search(baseDN, filter.toString(), ldapEntryConfig);
                }

                for (SearchResult result : search) {
                    if (!result.getNameInNamespace().equals(baseDN)) {
                        results.add((V) populateAttributedType(result, null));
                    }
                }
            }
        } catch (Exception e) {
            throw new ModelException("Querying of identity type failed " + identityQuery, e);
        }

        return results;
    }

    @Override
    public <V extends IdentityType> int countQueryResults(IdentityQuery<V> identityQuery) {
        int limit = identityQuery.getLimit();
        int offset = identityQuery.getOffset();

        identityQuery.setLimit(0);
        identityQuery.setOffset(0);

        int resultCount = identityQuery.getResultList().size();

        identityQuery.setLimit(limit);
        identityQuery.setOffset(offset);

        return resultCount;
    }

    public IdentityQueryBuilder createQueryBuilder() {
        return new DefaultQueryBuilder(this);
    }

    // *************** CREDENTIALS AND USER SPECIFIC STUFF

    @Override
    public boolean validatePassword(LDAPUser user, String password) {
        String userDN = getEntryDNOfUser(user);

        if (logger.isDebugEnabled()) {
            logger.debugf("Using DN [%s] for authentication of user [%s]", userDN, user.getLoginName());
        }

        if (operationManager.authenticate(userDN, password)) {
            return true;
        }

        return false;
    }

    @Override
    public void updatePassword(LDAPUser user, String password) {
        String userDN = getEntryDNOfUser(user);

        if (logger.isDebugEnabled()) {
            logger.debugf("Using DN [%s] for updating LDAP password of user [%s]", userDN, user.getLoginName());
        }

        if (getConfig().isActiveDirectory()) {
            updateADPassword(userDN, password);
        } else {
            ModificationItem[] mods = new ModificationItem[1];

            try {
                BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);

                mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);

                operationManager.modifyAttribute(userDN, mod0);
            } catch (Exception e) {
                throw new ModelException("Error updating password.", e);
            }
        }
    }


    private void updateADPassword(String userDN, String password) {
        try {
            // Replace the "unicdodePwd" attribute with a new value
            // Password must be both Unicode and a quoted string
            String newQuotedPassword = "\"" + password + "\"";
            byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");

            BasicAttribute unicodePwd = new BasicAttribute("unicodePwd", newUnicodePassword);

            List<ModificationItem> modItems = new ArrayList<ModificationItem>();
            modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));

            // Used in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored
            if (getConfig().isUserAccountControlsAfterPasswordUpdate()) {
                BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512");
                modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl));

                logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN);
            }

            operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
        } catch (Exception e) {
            throw new ModelException(e);
        }
    }


    private String getEntryDNOfUser(LDAPUser user) {
        // First try if user already has entryDN on him
        String entryDN = user.getEntryDN();
        if (entryDN != null) {
            return entryDN;
        }

        // Need to find user in LDAP
        String username = user.getLoginName();
        user = getUser(username);
        if (user == null) {
            throw new ModelException("No LDAP user found with username " + username);
        }

        return user.getEntryDN();
    }


    public LDAPUser getUser(String username) {

        if (isNullOrEmpty(username)) {
            return null;
        }

        IdentityQueryBuilder queryBuilder = createQueryBuilder();
        List<LDAPUser> agents = queryBuilder.createIdentityQuery(LDAPUser.class)
                .where(queryBuilder.equal(LDAPUser.LOGIN_NAME, username)).getResultList();

        if (agents.isEmpty()) {
            return null;
        } else if (agents.size() == 1) {
            return agents.get(0);
        } else {
            throw new ModelDuplicateException("Error - multiple Agent objects found with same login name");
        }
    }

    // ************ END CREDENTIALS AND USER SPECIFIC STUFF


    private String getBaseDN(final LDAPMappingConfiguration ldapEntryConfig) {
        String baseDN = getConfig().getBaseDN();

        if (ldapEntryConfig.getBaseDN() != null) {
            baseDN = ldapEntryConfig.getBaseDN();
        }

        return baseDN;
    }

    protected <V extends IdentityType> StringBuilder createIdentityTypeSearchFilter(final IdentityQuery<V> identityQuery, final LDAPMappingConfiguration ldapEntryConfig) {
        StringBuilder filter = new StringBuilder();

        for (Condition condition : identityQuery.getConditions()) {
            QueryParameter queryParameter = condition.getParameter();

            if (!IdentityType.ID.equals(queryParameter)) {
                if (AttributeParameter.class.isInstance(queryParameter)) {
                    AttributeParameter attributeParameter = (AttributeParameter) queryParameter;
                    String attributeName = ldapEntryConfig.getMappedProperties().get(attributeParameter.getName());

                    if (attributeName != null) {
                        if (EqualCondition.class.isInstance(condition)) {
                            EqualCondition equalCondition = (EqualCondition) condition;
                            Object parameterValue = equalCondition.getValue();

                            if (Date.class.isInstance(parameterValue)) {
                                parameterValue = LDAPUtil.formatDate((Date) parameterValue);
                            }

                            filter.append("(").append(attributeName).append(LDAPConstants.EQUAL).append(parameterValue).append(")");
                        } else if (LikeCondition.class.isInstance(condition)) {
                            LikeCondition likeCondition = (LikeCondition) condition;
                            String parameterValue = (String) likeCondition.getValue();

                        } else if (GreaterThanCondition.class.isInstance(condition)) {
                            GreaterThanCondition greaterThanCondition = (GreaterThanCondition) condition;
                            Comparable parameterValue = (Comparable) greaterThanCondition.getValue();

                            if (Date.class.isInstance(parameterValue)) {
                                parameterValue = LDAPUtil.formatDate((Date) parameterValue);
                            }

                            if (greaterThanCondition.isOrEqual()) {
                                filter.append("(").append(attributeName).append(">=").append(parameterValue).append(")");
                            } else {
                                filter.append("(").append(attributeName).append(">").append(parameterValue).append(")");
                            }
                        } else if (LessThanCondition.class.isInstance(condition)) {
                            LessThanCondition lessThanCondition = (LessThanCondition) condition;
                            Comparable parameterValue = (Comparable) lessThanCondition.getValue();

                            if (Date.class.isInstance(parameterValue)) {
                                parameterValue = LDAPUtil.formatDate((Date) parameterValue);
                            }

                            if (lessThanCondition.isOrEqual()) {
                                filter.append("(").append(attributeName).append("<=").append(parameterValue).append(")");
                            } else {
                                filter.append("(").append(attributeName).append("<").append(parameterValue).append(")");
                            }
                        } else if (BetweenCondition.class.isInstance(condition)) {
                            BetweenCondition betweenCondition = (BetweenCondition) condition;
                            Comparable x = betweenCondition.getX();
                            Comparable y = betweenCondition.getY();

                            if (Date.class.isInstance(x)) {
                                x = LDAPUtil.formatDate((Date) x);
                            }

                            if (Date.class.isInstance(y)) {
                                y = LDAPUtil.formatDate((Date) y);
                            }

                            filter.append("(").append(x).append("<=").append(attributeName).append("<=").append(y).append(")");
                        } else if (InCondition.class.isInstance(condition)) {
                            InCondition inCondition = (InCondition) condition;
                            Object[] valuesToCompare = inCondition.getValue();

                            filter.append("(&(");

                            for (int i = 0; i< valuesToCompare.length; i++) {
                                Object value = valuesToCompare[i];

                                filter.append("(").append(attributeName).append(LDAPConstants.EQUAL).append(value).append(")");
                            }

                            filter.append("))");
                        } else {
                            throw new ModelException("Unsupported query condition [" + condition + "].");
                        }
                    }
                }
            }
        }


        filter.insert(0, "(&(");
        filter.append(getObjectClassesFilter(ldapEntryConfig));
        filter.append("))");

        return filter;
    }

    private StringBuilder getObjectClassesFilter(final LDAPMappingConfiguration ldapEntryConfig) {
        StringBuilder builder = new StringBuilder();

        if (ldapEntryConfig != null && !ldapEntryConfig.getObjectClasses().isEmpty()) {
            for (String objectClass : ldapEntryConfig.getObjectClasses()) {
                builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
            }
        } else {
            builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
        }

        return builder;
    }

    private AttributedType populateAttributedType(SearchResult searchResult, AttributedType attributedType) {
        return populateAttributedType(searchResult, attributedType, 0);
    }

    private AttributedType populateAttributedType(SearchResult searchResult, AttributedType attributedType, int hierarchyDepthCount) {
        try {
            String entryDN = searchResult.getNameInNamespace();
            Attributes attributes = searchResult.getAttributes();

            if (attributedType == null) {
                attributedType = Reflections.newInstance(getConfig().getSupportedTypeByBaseDN(entryDN, getEntryObjectClasses(attributes)));
            }

            attributedType.setEntryDN(entryDN);

            LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());

            if (hierarchyDepthCount > mappingConfig.getHierarchySearchDepth()) {
                return null;
            }

            if (logger.isTraceEnabled()) {
                logger.tracef("Populating attributed type [%s] from DN [%s]", attributedType, entryDN);
            }

            NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();

            while (ldapAttributes.hasMore()) {
                Attribute ldapAttribute = ldapAttributes.next();
                Object attributeValue;

                try {
                    attributeValue = ldapAttribute.get();
                } catch (NoSuchElementException nsee) {
                    continue;
                }

                String ldapAttributeName = ldapAttribute.getID();

                if (ldapAttributeName.toLowerCase().equals(getConfig().getUniqueIdentifierAttributeName().toLowerCase())) {
                    attributedType.setId(this.operationManager.decodeEntryUUID(attributeValue));
                } else {
                    String attributeName = findAttributeName(mappingConfig.getMappedProperties(), ldapAttributeName);

                    if (attributeName != null) {
                        // Find if it's java property or attribute
                        Property<Object> property = PropertyQueries
                                .createQuery(attributedType.getClass())
                                .addCriteria(new NamedPropertyCriteria(attributeName)).getFirstResult();

                        if (property != null) {
                            if (logger.isTraceEnabled()) {
                                logger.tracef("Populating property [%s] from ldap attribute [%s] with value [%s] from DN [%s].", property.getName(), ldapAttributeName, attributeValue, entryDN);
                            }

                            if (property.getJavaClass().equals(Date.class)) {
                                property.setValue(attributedType, LDAPUtil.parseDate(attributeValue.toString()));
                            } else {
                                property.setValue(attributedType, attributeValue);
                            }
                        } else {
                            if (logger.isTraceEnabled()) {
                                logger.tracef("Populating attribute [%s] from ldap attribute [%s] with value [%s] from DN [%s].", attributeName, ldapAttributeName, attributeValue, entryDN);
                            }

                            attributedType.setAttribute(new org.keycloak.federation.ldap.idm.model.Attribute(attributeName, (Serializable) attributeValue));
                        }
                    }
                }
            }

            if (IdentityType.class.isInstance(attributedType)) {
                IdentityType identityType = (IdentityType) attributedType;

                String createdTimestamp = attributes.get(LDAPConstants.CREATE_TIMESTAMP).get().toString();

                identityType.setCreatedDate(LDAPUtil.parseDate(createdTimestamp));
            }

            LDAPMappingConfiguration entryConfig = getMappingConfig(attributedType.getClass());

            if (mappingConfig.getParentMembershipAttributeName() != null) {
                StringBuilder filter = new StringBuilder("(&");
                String entryBaseDN = entryDN.substring(entryDN.indexOf(LDAPConstants.COMMA) + 1);

                filter
                        .append("(")
                        .append(getObjectClassesFilter(entryConfig))
                        .append(")")
                        .append("(")
                        .append(mappingConfig.getParentMembershipAttributeName())
                        .append(LDAPConstants.EQUAL).append("")
                        .append(getBindingDN(attributedType, false))
                        .append(LDAPConstants.COMMA)
                        .append(entryBaseDN)
                        .append(")");

                filter.append(")");

                if (logger.isTraceEnabled()) {
                    logger.tracef("Searching parent entry for DN [%s] using filter [%s].", entryBaseDN, filter.toString());
                }

                List<SearchResult> search = this.operationManager.search(getConfig().getBaseDN(), filter.toString(), entryConfig);

                if (!search.isEmpty()) {
                    SearchResult next = search.get(0);

                    Property<AttributedType> parentProperty = PropertyQueries
                            .<AttributedType>createQuery(attributedType.getClass())
                            .addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult();

                    if (parentProperty != null) {
                        String parentDN = next.getNameInNamespace();
                        String parentBaseDN = parentDN.substring(parentDN.indexOf(",") + 1);
                        Class<? extends AttributedType> baseDNType = getConfig().getSupportedTypeByBaseDN(parentBaseDN, getEntryObjectClasses(attributes));

                        if (parentProperty.getJavaClass().isAssignableFrom(baseDNType)) {
                            if (logger.isTraceEnabled()) {
                                logger.tracef("Found parent [%s] for entry for DN [%s].", parentDN, entryDN);
                            }

                            int hierarchyDepthCount1 = ++hierarchyDepthCount;

                            parentProperty.setValue(attributedType, populateAttributedType(next, null, hierarchyDepthCount1));
                        }
                    }
                } else {
                    if (logger.isTraceEnabled()) {
                        logger.tracef("No parent entry found for DN [%s] using filter [%s].", entryDN, filter.toString());
                    }
                }
            }
        } catch (Exception e) {
            throw new ModelException("Could not populate attribute type " + attributedType + ".", e);
        }

        return attributedType;
    }

    private String findAttributeName(Map<String, String> attrMapping, String ldapAttributeName) {
        for (Map.Entry<String,String> currentAttr : attrMapping.entrySet()) {
            if (currentAttr.getValue().equalsIgnoreCase(ldapAttributeName)) {
                return currentAttr.getKey();
            }
        }

        return null;
    }

    private List<String> getEntryObjectClasses(final Attributes attributes) throws NamingException {
        Attribute objectClassesAttribute = attributes.get(LDAPConstants.OBJECT_CLASS);
        List<String> objectClasses = new ArrayList<String>();

        if (objectClassesAttribute == null) {
            return objectClasses;
        }

        NamingEnumeration<?> all = objectClassesAttribute.getAll();

        while (all.hasMore()) {
            objectClasses.add(all.next().toString());
        }

        return objectClasses;
    }

    protected BasicAttributes extractAttributes(AttributedType attributedType, boolean isCreate) {
        BasicAttributes entryAttributes = new BasicAttributes();
        LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
        Map<String, String> mappedProperties = mappingConfig.getMappedProperties();

        for (String propertyName : mappedProperties.keySet()) {
            if (!mappingConfig.getReadOnlyAttributes().contains(propertyName) && (isCreate || !mappingConfig.getBindingProperty().getName().equals(propertyName))) {
                Property<Object> property = PropertyQueries
                        .<Object>createQuery(attributedType.getClass())
                        .addCriteria(new NamedPropertyCriteria(propertyName)).getFirstResult();

                Object propertyValue = null;
                if (property != null) {
                    // Mapped Java property on the object
                    propertyValue = property.getValue(attributedType);
                } else {
                    // Not mapped property. So fallback to attribute
                    org.keycloak.federation.ldap.idm.model.Attribute<?> attribute = attributedType.getAttribute(propertyName);
                    if (attribute != null) {
                        propertyValue = attribute.getValue();
                    }
                }

                if (AttributedType.class.isInstance(propertyValue)) {
                    AttributedType referencedType = (AttributedType) propertyValue;
                    propertyValue = getBindingDN(referencedType, true);
                } else {
                    if (propertyValue == null || isNullOrEmpty(propertyValue.toString())) {
                        propertyValue = EMPTY_ATTRIBUTE_VALUE;
                    }
                }

                entryAttributes.put(mappedProperties.get(propertyName), propertyValue);
            }
        }

        // Don't extract object classes for update
        if (isCreate) {
            LDAPMappingConfiguration ldapEntryConfig = getMappingConfig(attributedType.getClass());

            BasicAttribute objectClassAttribute = new BasicAttribute(LDAPConstants.OBJECT_CLASS);

            for (String objectClassValue : ldapEntryConfig.getObjectClasses()) {
                objectClassAttribute.add(objectClassValue);

                if (objectClassValue.equals(LDAPConstants.GROUP_OF_NAMES)
                        || objectClassValue.equals(LDAPConstants.GROUP_OF_ENTRIES)
                        || objectClassValue.equals(LDAPConstants.GROUP_OF_UNIQUE_NAMES)) {
                    entryAttributes.put(LDAPConstants.MEMBER, EMPTY_ATTRIBUTE_VALUE);
                }
            }

            entryAttributes.put(objectClassAttribute);
        }

        return entryAttributes;
    }

    // TODO: Move class StringUtil from SAML module
    public static boolean isNullOrEmpty(String str) {
        return str == null || str.isEmpty();
    }

    private LDAPMappingConfiguration getMappingConfig(Class<? extends AttributedType> attributedType) {
        LDAPMappingConfiguration mappingConfig = getConfig().getMappingConfig(attributedType);

        if (mappingConfig == null) {
            throw new ModelException("Not mapped type [" + attributedType + "].");
        }

        return mappingConfig;
    }

    public String getBindingDN(AttributedType attributedType, boolean appendBaseDN) {
        LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
        Property<String> idProperty = mappingConfig.getIdProperty();

        String baseDN;

        if (mappingConfig.getBaseDN() == null || !appendBaseDN) {
            baseDN = "";
        } else {
            baseDN = LDAPConstants.COMMA + getBaseDN(attributedType);
        }

        Property<String> bindingProperty = mappingConfig.getBindingProperty();
        String bindingAttribute;
        String dn;

        if (bindingProperty == null) {
            bindingAttribute = mappingConfig.getMappedProperties().get(idProperty.getName());
            dn = idProperty.getValue(attributedType);
        } else {
            bindingAttribute = mappingConfig.getMappedProperties().get(bindingProperty.getName());
            dn = mappingConfig.getBindingProperty().getValue(attributedType);
        }

        return bindingAttribute + LDAPConstants.EQUAL + dn + baseDN;
    }

    private String getBaseDN(AttributedType attributedType) {
        LDAPMappingConfiguration mappingConfig = getMappingConfig(attributedType.getClass());
        String baseDN = mappingConfig.getBaseDN();
        String parentDN = mappingConfig.getParentMapping().get(mappingConfig.getIdProperty().getValue(attributedType));

        if (parentDN != null) {
            baseDN = parentDN;
        } else {
            Property<AttributedType> parentProperty = PropertyQueries
                    .<AttributedType>createQuery(attributedType.getClass())
                    .addCriteria(new TypedPropertyCriteria(attributedType.getClass())).getFirstResult();

            if (parentProperty != null) {
                AttributedType parentType = parentProperty.getValue(attributedType);

                if (parentType != null) {
                    Property<String> parentIdProperty = getMappingConfig(parentType.getClass()).getIdProperty();

                    String parentId = parentIdProperty.getValue(parentType);

                    String parentBaseDN = mappingConfig.getParentMapping().get(parentId);

                    if (parentBaseDN != null) {
                        baseDN = parentBaseDN;
                    } else {
                        baseDN = getBaseDN(parentType);
                    }
                }
            }
        }

        if (baseDN == null) {
            baseDN = getConfig().getBaseDN();
        }

        return baseDN;
    }

    protected void addToParentAsMember(final AttributedType attributedType) {
        LDAPMappingConfiguration entryConfig = getMappingConfig(attributedType.getClass());

        if (entryConfig.getParentMembershipAttributeName() != null) {
            Property<AttributedType> parentProperty = PropertyQueries
                    .<AttributedType>createQuery(attributedType.getClass())
                    .addCriteria(new TypedPropertyCriteria(attributedType.getClass()))
                    .getFirstResult();

            if (parentProperty != null) {
                AttributedType parentType = parentProperty.getValue(attributedType);

                if (parentType != null) {
                    Attributes attributes = this.operationManager.getAttributes(parentType.getId(), getBaseDN(parentType), entryConfig);
                    Attribute attribute = attributes.get(entryConfig.getParentMembershipAttributeName());

                    attribute.add(getBindingDN(attributedType, true));

                    this.operationManager.modifyAttribute(getBindingDN(parentType, true), attribute);
                }
            }
        }
    }

    protected String getEntryIdentifier(final AttributedType attributedType) {
        try {
            // we need this to retrieve the entry's identifier from the ldap server
            List<SearchResult> search = this.operationManager.search(getBaseDN(attributedType), "(" + getBindingDN(attributedType, false) + ")", getMappingConfig(attributedType.getClass()));
            Attribute id = search.get(0).getAttributes().get(getConfig().getUniqueIdentifierAttributeName());

            if (id == null) {
                throw new ModelException("Could not retrieve identifier for entry [" + getBindingDN(attributedType, true) + "].");
            }

            return this.operationManager.decodeEntryUUID(id.get());
        } catch (NamingException ne) {
            throw new ModelException("Could not add type [" + attributedType + "].", ne);
        }
    }
}
