<commit_before>/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.plugin.ldap;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.cache.eviction.LRUEvictionConfiguration;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.syntax.Syntax;

import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPDN;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import com.novell.ldap.rfc2251.RfcFilter;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.user.impl.LDAP.LDAPProfileXClass;
import com.xpn.xwiki.web.Utils;

/**
 * LDAP communication tool.
 * 
 * @version $Id$
 * @since 1.3 M2
 */
public class XWikiLDAPUtils
{
    /**
     * Logging tool.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiLDAPUtils.class);

    /**
     * LDAP objectClass parameter.
     */
    private static final String LDAP_OBJECTCLASS = "objectClass";

    /**
     * The name of the LDAP groups cache.
     */
    private static final String CACHE_NAME_GROUPS = "ldap.groups";

    /**
     * The name of the XWiki group member field.
     */
    private static final String XWIKI_GROUP_MEMBERFIELD = "member";

    /**
     * The XWiki space where users are stored.
     */
    private static final String XWIKI_USER_SPACE = "XWiki";

    /**
     * The configuration of the LDAP group cache.
     */
    private static CacheConfiguration cacheConfigurationGroups;

    /**
     * Default unique user field name.
     */
    private static final String LDAP_DEFAULT_UID = "cn";

    /**
     * The name of the LDAP object field "dn".
     */
    private static final String LDAP_FIELD_DN = "dn";

    /**
     * Contains caches for each LDAP host:port.
     */
    private static Map<String, Map<String, Cache<Map<String, String>>>> cachePool =
        new HashMap<String, Map<String, Cache<Map<String, String>>>>();

    /**
     * The LDAP connection.
     */
    private XWikiLDAPConnection connection;

    /**
     * The LDAP attribute containing the identifier for a user.
     */
    private String uidAttributeName = LDAP_DEFAULT_UID;

    /**
     * Different LDAP implementations groups classes names.
     */
    private Collection<String> groupClasses = XWikiLDAPConfig.DEFAULT_GROUP_CLASSES;

    /**
     * Different LDAP implementations groups member property name.
     */
    private Collection<String> groupMemberFields = XWikiLDAPConfig.DEFAULT_GROUP_MEMBERFIELDS;

    /**
     * The LDAP base DN from where to executes LDAP queries.
     */
    private String baseDN = "";

    /**
     * LDAP search format string.
     */
    private String userSearchFormatString = "({0}={1})";

    /**
     * @see #isResolveSubgroups()
     */
    private boolean resolveSubgroups = true;

    /**
     * Create an instance of {@link XWikiLDAPUtils}.
     * 
     * @param connection the XWiki LDAP connection tool.
     */
    public XWikiLDAPUtils(XWikiLDAPConnection connection)
    {
        this.connection = connection;
    }

    /**
     * @param uidAttributeName the LDAP attribute containing the identifier for a user.
     */
    public void setUidAttributeName(String uidAttributeName)
    {
        this.uidAttributeName = uidAttributeName;
    }

    /**
     * @return the LDAP attribute containing the identifier for a user.
     */
    public String getUidAttributeName()
    {
        return this.uidAttributeName;
    }

    /**
     * @param baseDN the LDAP base DN from where to executes LDAP queries.
     */
    public void setBaseDN(String baseDN)
    {
        this.baseDN = baseDN;
    }

    /**
     * @return the LDAP base DN from where to executes LDAP queries.
     */
    public String getBaseDN()
    {
        return this.baseDN;
    }

    /**
     * @param fmt the user search format string.
     */
    public void setUserSearchFormatString(String fmt)
    {
        this.userSearchFormatString = fmt;
    }

    /**
     * @return the user search format string.
     */
    public String getUserSearchFormatString()
    {
        return this.userSearchFormatString;
    }

    /**
     * @param groupClasses the different LDAP implementations groups classes names.
     */
    public void setGroupClasses(Collection<String> groupClasses)
    {
        this.groupClasses = groupClasses;
    }

    /**
     * @return the different LDAP implementations groups classes names.
     */
    public Collection<String> getGroupClasses()
    {
        return this.groupClasses;
    }

    /**
     * @param groupMemberFields the different LDAP implementations groups member property name.
     */
    public void setGroupMemberFields(Collection<String> groupMemberFields)
    {
        this.groupMemberFields = groupMemberFields;
    }

    /**
     * @return the different LDAP implementations groups member property name.
     */
    public Collection<String> getGroupMemberFields()
    {
        return this.groupMemberFields;
    }

    /**
     * @return true if sub groups should be resolved too
     */
    public boolean isResolveSubgroups()
    {
        return this.resolveSubgroups;
    }

    /**
     * @param resolveSubgroups true if sub groups should be resolved too
     */
    public void setResolveSubgroups(boolean resolveSubgroups)
    {
        this.resolveSubgroups = resolveSubgroups;
    }

    /**
     * Get the cache with the provided name for a particular LDAP server.
     * 
     * @param configuration the configuration to use to create the cache and to find it if it's already created.
     * @param context the XWiki context.
     * @return the cache.
     * @throws CacheException error when creating the cache.
     * @deprecated use {@link #getGroupCache(CacheConfiguration, XWikiContext)} instead since 4.1M1
     */
    @Deprecated
    public Cache<Map<String, String>> getCache(CacheConfiguration configuration, XWikiContext context)
        throws CacheException
    {
        return getGroupCache(configuration, context);
    }

    /**
     * Get the cache with the provided name for a particular LDAP server.
     * 
     * @param configuration the configuration to use to create the cache and to find it if it's already created.
     * @param context the XWiki context.
     * @return the cache.
     * @throws CacheException error when creating the cache.
     * @since 4.1M1
     */
    public Cache<Map<String, String>> getGroupCache(CacheConfiguration configuration, XWikiContext context)
        throws CacheException
    {
        Cache<Map<String, String>> cache;

        String cacheKey =
            getUidAttributeName() + "." + this.connection.getConnection().getHost() + ":"
                + this.connection.getConnection().getPort();

        synchronized (cachePool) {
            Map<String, Cache<Map<String, String>>> cacheMap;

            if (cachePool.containsKey(cacheKey)) {
                cacheMap = cachePool.get(cacheKey);
            } else {
                cacheMap = new HashMap<String, Cache<Map<String, String>>>();
                cachePool.put(cacheKey, cacheMap);
            }

            cache = cacheMap.get(configuration.getConfigurationId());

            if (cache == null) {
                cache = Utils.getComponent(CacheManager.class).createNewCache(configuration);
                cacheMap.put(configuration.getConfigurationId(), cache);
            }
        }

        return cache;
    }

    /**
     * Force to empty the group cache.
     * 
     * @since 4.1M1
     */
    public static void resetGroupCache()
    {
        synchronized (cachePool) {
            for (Map<String, Cache<Map<String, String>>> caches : cachePool.values()) {
                for (Cache<Map<String, String>> cache : caches.values()) {
                    cache.dispose();
                }
            }
        }

        cachePool.clear();
    }

    /**
     * @return get {@link XWikiLDAPConnection}.
     */
    public XWikiLDAPConnection getConnection()
    {
        return this.connection;
    }

    /**
     * Execute LDAP query to get all group's members.
     * 
     * @param groupDN the group to retrieve the members of and scan for subgroups.
     * @return the LDAP search result.
     * @throws LDAPException failed to execute LDAP query
     */
    private LDAPSearchResults searchGroupsMembersByDN(String groupDN) throws LDAPException
    {
        String[] attrs = new String[2 + getGroupMemberFields().size()];

        int i = 0;
        attrs[i++] = LDAP_OBJECTCLASS;
        for (String groupMember : getGroupMemberFields()) {
            attrs[i++] = groupMember;
        }

        // in case it's a organization unit get the users ids
        attrs[i++] = getUidAttributeName();

        return getConnection().search(groupDN, null, attrs, LDAPConnection.SCOPE_SUB);
    }

    /**
     * Execute LDAP query to get all group's members.
     * 
     * @param filter the LDAP filter to search with.
     * @return the LDAP search result.
     * @throws LDAPException failed to execute LDAP query
     */
    private LDAPSearchResults searchGroupsMembersByFilter(String filter) throws LDAPException
    {
        String[] attrs = new String[2 + getGroupMemberFields().size()];

        int i = 0;
        attrs[i++] = LDAP_OBJECTCLASS;
        for (String groupMember : getGroupMemberFields()) {
            attrs[i++] = groupMember;
        }

        // in case it's a organization unit get the users ids
        attrs[i++] = getUidAttributeName();

        return getConnection().search(getBaseDN(), filter, attrs, LDAPConnection.SCOPE_SUB);
    }

    /**
     * Extract group's members from provided LDAP search result.
     * 
     * @param searchAttributeList the LDAP search result.
     * @param memberMap the result: maps DN to member id.
     * @param subgroups return all the subgroups identified.
     * @param context the XWiki context.
     */
    private void getGroupMembersFromSearchResult(List<XWikiLDAPSearchAttribute> searchAttributeList,
        Map<String, String> memberMap, List<String> subgroups, XWikiContext context)
    {
        for (XWikiLDAPSearchAttribute searchAttribute : searchAttributeList) {
            String key = searchAttribute.name;
            if (getGroupMemberFields().contains(key.toLowerCase())) {

                // or subgroup
                String member = searchAttribute.value;

                // we check for subgroups recursive call to scan all subgroups and identify members
                // and their uid
                getGroupMembers(member, memberMap, subgroups, context);
            }
        }
    }

    /**
     * Extract group's members from provided LDAP search result.
     * 
     * @param ldapEntry the LDAP search result.
     * @param memberMap the result: maps DN to member id.
     * @param subgroups return all the subgroups identified.
     * @param context the XWiki context.
     */
    private void getGroupMembersFromLDAPEntry(LDAPEntry ldapEntry, Map<String, String> memberMap,
        List<String> subgroups, XWikiContext context)
    {
        for (String memberField : getGroupMemberFields()) {
            LDAPAttribute attribute = ldapEntry.getAttribute(memberField);
            if (attribute != null) {
                Enumeration<String> values = attribute.getStringValues();
                while (values.hasMoreElements()) {
                    String member = values.nextElement();

                    LOGGER.debug("  |- Member value [{}] found. Trying to resolve it.", member);

                    // we check for subgroups recursive call to scan all subgroups and identify members
                    // and their uid
                    getGroupMembers(member, memberMap, subgroups, context);
                }
            }
        }
    }

    /**
     * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well.
     * Retrieve an identifier for each member.
     * 
     * @param groupDN the group to retrieve the members of and scan for subgroups.
     * @param memberMap the result: maps DN to member id.
     * @param subgroups all the subgroups identified.
     * @param searchAttributeList the groups members found in LDAP search.
     * @param context the XWiki context.
     * @return whether the groupDN is actually a group.
     */
    public boolean getGroupMembers(String groupDN, Map<String, String> memberMap, List<String> subgroups,
        List<XWikiLDAPSearchAttribute> searchAttributeList, XWikiContext context)
    {
        boolean isGroup = false;

        String id = null;

        for (XWikiLDAPSearchAttribute searchAttribute : searchAttributeList) {
            String key = searchAttribute.name;

            if (key.equalsIgnoreCase(LDAP_OBJECTCLASS)) {
                String objectName = searchAttribute.value;
                if (getGroupClasses().contains(objectName.toLowerCase())) {
                    isGroup = true;
                }
            } else if (key.equalsIgnoreCase(getUidAttributeName())) {
                id = searchAttribute.value;
            }
        }

        if (!isGroup) {
            if (id == null) {
                LOGGER.error("Could not find attribute [{}] for LDAP dn [{}]", getUidAttributeName(), groupDN);
            }

            if (!memberMap.containsKey(groupDN.toLowerCase())) {
                memberMap.put(groupDN.toLowerCase(), id == null ? "" : id.toLowerCase());
            }
        } else {
            // remember this group
            if (subgroups != null) {
                subgroups.add(groupDN.toLowerCase());
            }

            getGroupMembersFromSearchResult(searchAttributeList, memberMap, subgroups, context);
        }

        return isGroup;
    }

    /**
     * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well.
     * Retrieve an identifier for each member.
     * 
     * @param memberMap the result: maps DN to member id.
     * @param subgroups all the subgroups identified.
     * @param ldapEntry the ldap entry returned by a search members found in LDAP search.
     * @param context the XWiki context.
     * @return whether the groupDN is actually a group.
     * @throws LDAPException error when parsing the provided LDAP entry
     * @since 3.3M1
     */
    public boolean getGroupMembers(Map<String, String> memberMap, List<String> subgroups, LDAPEntry ldapEntry,
        XWikiContext context) throws LDAPException
    {
        boolean isGroup = false;

        // Check if the entry is a group

        LDAPAttribute classAttribute = ldapEntry.getAttribute(LDAP_OBJECTCLASS);
        if (classAttribute != null) {
            Enumeration<String> values = classAttribute.getStringValues();
            Collection<String> groupClasses = getGroupClasses();
            while (values.hasMoreElements()) {
                String value = values.nextElement();
                if (groupClasses.contains(value.toLowerCase())) {
                    isGroup = true;
                }
            }
        }

        // Get members or user id if it's a user

        if (isGroup) {
            // remember this group
            if (subgroups != null) {
                subgroups.add(ldapEntry.getDN().toLowerCase());
            }

            getGroupMembersFromLDAPEntry(ldapEntry, memberMap, subgroups, context);
        } else {
            LDAPAttribute uidAttribute = ldapEntry.getAttribute(getUidAttributeName());

            if (uidAttribute != null) {
                String uid = uidAttribute.getStringValue();

                if (!memberMap.containsKey(ldapEntry.getDN().toLowerCase())) {
                    memberMap.put(ldapEntry.getDN().toLowerCase(), uid.toLowerCase());
                }
            } else {
                LOGGER.debug("Probably a organization unit or a search");
            }
        }

        return isGroup;
    }

    /**
     * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well.
     * Retrieve an identifier for each member.
     * 
     * @param userOrGroup the group to retrieve the members of and scan for subgroups. Can be
     *            <ul>
     *            <li>a group DN</li>
     *            <li>a user DN</li>
     *            <li>a group id</li>
     *            <li>a user id</li>
     *            </ul>
     * @param memberMap the result: maps DN to member id.
     * @param subgroups all the subgroups identified.
     * @param context the XWiki context.
     * @return whether the identifier is actually a group.
     */
    public boolean getGroupMembers(String userOrGroup, Map<String, String> memberMap, List<String> subgroups,
        XWikiContext context)
    {
        boolean isGroup = false;

        int nbMembers = memberMap.size();
        if (LDAPDN.isValid(userOrGroup)) {
            // Stop there if passed used is already a resolved member
            if (memberMap.containsKey(userOrGroup.toLowerCase())) {
                return false;
            }

            // Stop there if subgroup resolution is disabled
            if (!subgroups.isEmpty() && !isResolveSubgroups()) {
                memberMap.put(userOrGroup.toLowerCase(), userOrGroup);

                return false;
            }

            LOGGER.debug("[{}] is a valid DN, lets try to get corresponding entry.", userOrGroup);

            isGroup = getGroupMembersFromDN(userOrGroup, memberMap, subgroups, context);
        }

        if (!isGroup && nbMembers == memberMap.size()) {
            // Probably not a DN, lets try as filter or id
            LOGGER.debug("Looks like [{}] is not a DN, lets try filter or id", userOrGroup);

            try {
                // Test if it's valid LDAP filter syntax
                new RfcFilter(userOrGroup);
                isGroup = getGroupMembersFromFilter(userOrGroup, memberMap, subgroups, context);
            } catch (LDAPException e) {
                LOGGER.debug("[{}] is not a valid LDAP filter, lets try id", userOrGroup, e);

                // Not a valid filter, try as uid
                List<XWikiLDAPSearchAttribute> searchAttributeList =
                    searchUserAttributesByUid(userOrGroup, new String[] {LDAP_FIELD_DN});

                if (searchAttributeList != null && !searchAttributeList.isEmpty()) {
                    String dn = searchAttributeList.get(0).value;

                    // Stop there if passed used is already a resolved member
                    if (memberMap.containsKey(userOrGroup.toLowerCase())) {
                        return false;
                    }

                    // Stop there if subgroup resolution is disabled
                    if (!subgroups.isEmpty() && !isResolveSubgroups()) {
                        memberMap.put(dn.toLowerCase(), dn);

                        return false;
                    }

                    isGroup = getGroupMembers(dn, memberMap, subgroups, context);
                }
            }
        }<commit_msg>XWIKI-12413: LDAP Group Sync makes extra queries when duplicated member entry is a user * fix mistake * add more debug log<commit_after>/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.plugin.ldap;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.cache.eviction.LRUEvictionConfiguration;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.syntax.Syntax;

import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPDN;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchResults;
import com.novell.ldap.rfc2251.RfcFilter;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.user.impl.LDAP.LDAPProfileXClass;
import com.xpn.xwiki.web.Utils;

/**
 * LDAP communication tool.
 * 
 * @version $Id$
 * @since 1.3 M2
 */
public class XWikiLDAPUtils
{
    /**
     * Logging tool.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiLDAPUtils.class);

    /**
     * LDAP objectClass parameter.
     */
    private static final String LDAP_OBJECTCLASS = "objectClass";

    /**
     * The name of the LDAP groups cache.
     */
    private static final String CACHE_NAME_GROUPS = "ldap.groups";

    /**
     * The name of the XWiki group member field.
     */
    private static final String XWIKI_GROUP_MEMBERFIELD = "member";

    /**
     * The XWiki space where users are stored.
     */
    private static final String XWIKI_USER_SPACE = "XWiki";

    /**
     * The configuration of the LDAP group cache.
     */
    private static CacheConfiguration cacheConfigurationGroups;

    /**
     * Default unique user field name.
     */
    private static final String LDAP_DEFAULT_UID = "cn";

    /**
     * The name of the LDAP object field "dn".
     */
    private static final String LDAP_FIELD_DN = "dn";

    /**
     * Contains caches for each LDAP host:port.
     */
    private static Map<String, Map<String, Cache<Map<String, String>>>> cachePool =
        new HashMap<String, Map<String, Cache<Map<String, String>>>>();

    /**
     * The LDAP connection.
     */
    private XWikiLDAPConnection connection;

    /**
     * The LDAP attribute containing the identifier for a user.
     */
    private String uidAttributeName = LDAP_DEFAULT_UID;

    /**
     * Different LDAP implementations groups classes names.
     */
    private Collection<String> groupClasses = XWikiLDAPConfig.DEFAULT_GROUP_CLASSES;

    /**
     * Different LDAP implementations groups member property name.
     */
    private Collection<String> groupMemberFields = XWikiLDAPConfig.DEFAULT_GROUP_MEMBERFIELDS;

    /**
     * The LDAP base DN from where to executes LDAP queries.
     */
    private String baseDN = "";

    /**
     * LDAP search format string.
     */
    private String userSearchFormatString = "({0}={1})";

    /**
     * @see #isResolveSubgroups()
     */
    private boolean resolveSubgroups = true;

    /**
     * Create an instance of {@link XWikiLDAPUtils}.
     * 
     * @param connection the XWiki LDAP connection tool.
     */
    public XWikiLDAPUtils(XWikiLDAPConnection connection)
    {
        this.connection = connection;
    }

    /**
     * @param uidAttributeName the LDAP attribute containing the identifier for a user.
     */
    public void setUidAttributeName(String uidAttributeName)
    {
        this.uidAttributeName = uidAttributeName;
    }

    /**
     * @return the LDAP attribute containing the identifier for a user.
     */
    public String getUidAttributeName()
    {
        return this.uidAttributeName;
    }

    /**
     * @param baseDN the LDAP base DN from where to executes LDAP queries.
     */
    public void setBaseDN(String baseDN)
    {
        this.baseDN = baseDN;
    }

    /**
     * @return the LDAP base DN from where to executes LDAP queries.
     */
    public String getBaseDN()
    {
        return this.baseDN;
    }

    /**
     * @param fmt the user search format string.
     */
    public void setUserSearchFormatString(String fmt)
    {
        this.userSearchFormatString = fmt;
    }

    /**
     * @return the user search format string.
     */
    public String getUserSearchFormatString()
    {
        return this.userSearchFormatString;
    }

    /**
     * @param groupClasses the different LDAP implementations groups classes names.
     */
    public void setGroupClasses(Collection<String> groupClasses)
    {
        this.groupClasses = groupClasses;
    }

    /**
     * @return the different LDAP implementations groups classes names.
     */
    public Collection<String> getGroupClasses()
    {
        return this.groupClasses;
    }

    /**
     * @param groupMemberFields the different LDAP implementations groups member property name.
     */
    public void setGroupMemberFields(Collection<String> groupMemberFields)
    {
        this.groupMemberFields = groupMemberFields;
    }

    /**
     * @return the different LDAP implementations groups member property name.
     */
    public Collection<String> getGroupMemberFields()
    {
        return this.groupMemberFields;
    }

    /**
     * @return true if sub groups should be resolved too
     */
    public boolean isResolveSubgroups()
    {
        return this.resolveSubgroups;
    }

    /**
     * @param resolveSubgroups true if sub groups should be resolved too
     */
    public void setResolveSubgroups(boolean resolveSubgroups)
    {
        this.resolveSubgroups = resolveSubgroups;
    }

    /**
     * Get the cache with the provided name for a particular LDAP server.
     * 
     * @param configuration the configuration to use to create the cache and to find it if it's already created.
     * @param context the XWiki context.
     * @return the cache.
     * @throws CacheException error when creating the cache.
     * @deprecated use {@link #getGroupCache(CacheConfiguration, XWikiContext)} instead since 4.1M1
     */
    @Deprecated
    public Cache<Map<String, String>> getCache(CacheConfiguration configuration, XWikiContext context)
        throws CacheException
    {
        return getGroupCache(configuration, context);
    }

    /**
     * Get the cache with the provided name for a particular LDAP server.
     * 
     * @param configuration the configuration to use to create the cache and to find it if it's already created.
     * @param context the XWiki context.
     * @return the cache.
     * @throws CacheException error when creating the cache.
     * @since 4.1M1
     */
    public Cache<Map<String, String>> getGroupCache(CacheConfiguration configuration, XWikiContext context)
        throws CacheException
    {
        Cache<Map<String, String>> cache;

        String cacheKey =
            getUidAttributeName() + "." + this.connection.getConnection().getHost() + ":"
                + this.connection.getConnection().getPort();

        synchronized (cachePool) {
            Map<String, Cache<Map<String, String>>> cacheMap;

            if (cachePool.containsKey(cacheKey)) {
                cacheMap = cachePool.get(cacheKey);
            } else {
                cacheMap = new HashMap<String, Cache<Map<String, String>>>();
                cachePool.put(cacheKey, cacheMap);
            }

            cache = cacheMap.get(configuration.getConfigurationId());

            if (cache == null) {
                cache = Utils.getComponent(CacheManager.class).createNewCache(configuration);
                cacheMap.put(configuration.getConfigurationId(), cache);
            }
        }

        return cache;
    }

    /**
     * Force to empty the group cache.
     * 
     * @since 4.1M1
     */
    public static void resetGroupCache()
    {
        synchronized (cachePool) {
            for (Map<String, Cache<Map<String, String>>> caches : cachePool.values()) {
                for (Cache<Map<String, String>> cache : caches.values()) {
                    cache.dispose();
                }
            }
        }

        cachePool.clear();
    }

    /**
     * @return get {@link XWikiLDAPConnection}.
     */
    public XWikiLDAPConnection getConnection()
    {
        return this.connection;
    }

    /**
     * Execute LDAP query to get all group's members.
     * 
     * @param groupDN the group to retrieve the members of and scan for subgroups.
     * @return the LDAP search result.
     * @throws LDAPException failed to execute LDAP query
     */
    private LDAPSearchResults searchGroupsMembersByDN(String groupDN) throws LDAPException
    {
        String[] attrs = new String[2 + getGroupMemberFields().size()];

        int i = 0;
        attrs[i++] = LDAP_OBJECTCLASS;
        for (String groupMember : getGroupMemberFields()) {
            attrs[i++] = groupMember;
        }

        // in case it's a organization unit get the users ids
        attrs[i++] = getUidAttributeName();

        return getConnection().search(groupDN, null, attrs, LDAPConnection.SCOPE_SUB);
    }

    /**
     * Execute LDAP query to get all group's members.
     * 
     * @param filter the LDAP filter to search with.
     * @return the LDAP search result.
     * @throws LDAPException failed to execute LDAP query
     */
    private LDAPSearchResults searchGroupsMembersByFilter(String filter) throws LDAPException
    {
        String[] attrs = new String[2 + getGroupMemberFields().size()];

        int i = 0;
        attrs[i++] = LDAP_OBJECTCLASS;
        for (String groupMember : getGroupMemberFields()) {
            attrs[i++] = groupMember;
        }

        // in case it's a organization unit get the users ids
        attrs[i++] = getUidAttributeName();

        return getConnection().search(getBaseDN(), filter, attrs, LDAPConnection.SCOPE_SUB);
    }

    /**
     * Extract group's members from provided LDAP search result.
     * 
     * @param searchAttributeList the LDAP search result.
     * @param memberMap the result: maps DN to member id.
     * @param subgroups return all the subgroups identified.
     * @param context the XWiki context.
     */
    private void getGroupMembersFromSearchResult(List<XWikiLDAPSearchAttribute> searchAttributeList,
        Map<String, String> memberMap, List<String> subgroups, XWikiContext context)
    {
        for (XWikiLDAPSearchAttribute searchAttribute : searchAttributeList) {
            String key = searchAttribute.name;
            if (getGroupMemberFields().contains(key.toLowerCase())) {

                // or subgroup
                String member = searchAttribute.value;

                // we check for subgroups recursive call to scan all subgroups and identify members
                // and their uid
                getGroupMembers(member, memberMap, subgroups, context);
            }
        }
    }

    /**
     * Extract group's members from provided LDAP search result.
     * 
     * @param ldapEntry the LDAP search result.
     * @param memberMap the result: maps DN to member id.
     * @param subgroups return all the subgroups identified.
     * @param context the XWiki context.
     */
    private void getGroupMembersFromLDAPEntry(LDAPEntry ldapEntry, Map<String, String> memberMap,
        List<String> subgroups, XWikiContext context)
    {
        for (String memberField : getGroupMemberFields()) {
            LDAPAttribute attribute = ldapEntry.getAttribute(memberField);
            if (attribute != null) {
                Enumeration<String> values = attribute.getStringValues();
                while (values.hasMoreElements()) {
                    String member = values.nextElement();

                    LOGGER.debug("  |- Member value [{}] found. Trying to resolve it.", member);

                    // we check for subgroups recursive call to scan all subgroups and identify members
                    // and their uid
                    getGroupMembers(member, memberMap, subgroups, context);
                }
            }
        }
    }

    /**
     * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well.
     * Retrieve an identifier for each member.
     * 
     * @param groupDN the group to retrieve the members of and scan for subgroups.
     * @param memberMap the result: maps DN to member id.
     * @param subgroups all the subgroups identified.
     * @param searchAttributeList the groups members found in LDAP search.
     * @param context the XWiki context.
     * @return whether the groupDN is actually a group.
     */
    public boolean getGroupMembers(String groupDN, Map<String, String> memberMap, List<String> subgroups,
        List<XWikiLDAPSearchAttribute> searchAttributeList, XWikiContext context)
    {
        boolean isGroup = false;

        String id = null;

        for (XWikiLDAPSearchAttribute searchAttribute : searchAttributeList) {
            String key = searchAttribute.name;

            if (key.equalsIgnoreCase(LDAP_OBJECTCLASS)) {
                String objectName = searchAttribute.value;
                if (getGroupClasses().contains(objectName.toLowerCase())) {
                    isGroup = true;
                }
            } else if (key.equalsIgnoreCase(getUidAttributeName())) {
                id = searchAttribute.value;
            }
        }

        if (!isGroup) {
            if (id == null) {
                LOGGER.error("Could not find attribute [{}] for LDAP dn [{}]", getUidAttributeName(), groupDN);
            }

            if (!memberMap.containsKey(groupDN.toLowerCase())) {
                memberMap.put(groupDN.toLowerCase(), id == null ? "" : id.toLowerCase());
            }
        } else {
            // remember this group
            if (subgroups != null) {
                subgroups.add(groupDN.toLowerCase());
            }

            getGroupMembersFromSearchResult(searchAttributeList, memberMap, subgroups, context);
        }

        return isGroup;
    }

    /**
     * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well.
     * Retrieve an identifier for each member.
     * 
     * @param memberMap the result: maps DN to member id.
     * @param subgroups all the subgroups identified.
     * @param ldapEntry the ldap entry returned by a search members found in LDAP search.
     * @param context the XWiki context.
     * @return whether the groupDN is actually a group.
     * @throws LDAPException error when parsing the provided LDAP entry
     * @since 3.3M1
     */
    public boolean getGroupMembers(Map<String, String> memberMap, List<String> subgroups, LDAPEntry ldapEntry,
        XWikiContext context) throws LDAPException
    {
        boolean isGroup = false;

        // Check if the entry is a group

        LDAPAttribute classAttribute = ldapEntry.getAttribute(LDAP_OBJECTCLASS);
        if (classAttribute != null) {
            Enumeration<String> values = classAttribute.getStringValues();
            Collection<String> groupClasses = getGroupClasses();
            while (values.hasMoreElements()) {
                String value = values.nextElement();
                if (groupClasses.contains(value.toLowerCase())) {
                    isGroup = true;
                }
            }
        }

        // Get members or user id if it's a user

        if (isGroup) {
            // remember this group
            if (subgroups != null) {
                subgroups.add(ldapEntry.getDN().toLowerCase());
            }

            getGroupMembersFromLDAPEntry(ldapEntry, memberMap, subgroups, context);
        } else {
            LDAPAttribute uidAttribute = ldapEntry.getAttribute(getUidAttributeName());

            if (uidAttribute != null) {
                String uid = uidAttribute.getStringValue();

                if (!memberMap.containsKey(ldapEntry.getDN().toLowerCase())) {
                    memberMap.put(ldapEntry.getDN().toLowerCase(), uid.toLowerCase());
                }
            } else {
                LOGGER.debug("Probably a organization unit or a search");
            }
        }

        return isGroup;
    }

    /**
     * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well.
     * Retrieve an identifier for each member.
     * 
     * @param userOrGroup the group to retrieve the members of and scan for subgroups. Can be
     *            <ul>
     *            <li>a group DN</li>
     *            <li>a user DN</li>
     *            <li>a group id</li>
     *            <li>a user id</li>
     *            </ul>
     * @param memberMap the result: maps DN to member id.
     * @param subgroups all the subgroups identified.
     * @param context the XWiki context.
     * @return whether the identifier is actually a group.
     */
    public boolean getGroupMembers(String userOrGroup, Map<String, String> memberMap, List<String> subgroups,
        XWikiContext context)
    {
        boolean isGroup = false;

        int nbMembers = memberMap.size();
        if (LDAPDN.isValid(userOrGroup)) {
            LOGGER.debug("[{}] is a valid DN, lets try to get corresponding entry.", userOrGroup);

            // Stop there if passed used is already a resolved member
            if (memberMap.containsKey(userOrGroup.toLowerCase())) {
                LOGGER.debug("[{}] is already resolved", userOrGroup);

                return false;
            }

            // Stop there if subgroup resolution is disabled
            if (!subgroups.isEmpty() && !isResolveSubgroups()) {
                LOGGER.debug("Group members resolve is disabled to add [{}] as group member directly", userOrGroup);

                memberMap.put(userOrGroup.toLowerCase(), userOrGroup);

                return false;
            }

            isGroup = getGroupMembersFromDN(userOrGroup, memberMap, subgroups, context);
        }

        if (!isGroup && nbMembers == memberMap.size()) {
            // Probably not a DN, lets try as filter or id
            LOGGER.debug("Looks like [{}] is not a DN, lets try filter or id", userOrGroup);

            try {
                // Test if it's valid LDAP filter syntax
                new RfcFilter(userOrGroup);
                isGroup = getGroupMembersFromFilter(userOrGroup, memberMap, subgroups, context);
            } catch (LDAPException e) {
                LOGGER.debug("[{}] is not a valid LDAP filter, lets try id", userOrGroup, e);

                // Not a valid filter, try as uid
                List<XWikiLDAPSearchAttribute> searchAttributeList =
                    searchUserAttributesByUid(userOrGroup, new String[] {LDAP_FIELD_DN});

                if (searchAttributeList != null && !searchAttributeList.isEmpty()) {
                    String dn = searchAttributeList.get(0).value;

                    // Stop there if passed used is already a resolved member
                    if (memberMap.containsKey(dn.toLowerCase())) {
                        LOGGER.debug("[{}] is already resolved", dn);

                        return false;
                    }

                    // Stop there if subgroup resolution is disabled
                    if (!subgroups.isEmpty() && !isResolveSubgroups()) {
                        LOGGER.debug("Group members resolve is disabled to add [{}] as group member directly", dn);

                        memberMap.put(dn.toLowerCase(), dn);

                        return false;
                    }

                    isGroup = getGroupMembers(dn, memberMap, subgroups, context);
                }
            }
        }
{'fraction_non_alphanumeric': 0.08894022523823275, 'fraction_numerical': 0.00209356049667918, 'mean_word_length': 2.236274433455338, 'pattern_counts': {'":': 2, '<': 117, '<?xml version=': 0, '>': 117, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5402317', 'n_tokens_mistral': 11199, 'n_tokens_neox': 10765, 'n_words': 3393}