001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.datasourcesexplorer;
017
018import java.nio.charset.StandardCharsets;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027import java.util.stream.Collectors;
028
029import javax.naming.NamingEnumeration;
030import javax.naming.NamingException;
031import javax.naming.directory.Attribute;
032import javax.naming.directory.Attributes;
033import javax.naming.directory.SearchControls;
034import javax.naming.directory.SearchResult;
035import javax.naming.ldap.InitialLdapContext;
036import javax.naming.ldap.LdapContext;
037
038import org.apache.avalon.framework.component.Component;
039import org.apache.cocoon.ProcessingException;
040import org.apache.commons.lang3.StringUtils;
041
042import org.ametys.core.util.ldap.AbstractLDAPConnector;
043
044/**
045 * Connection stuff to read ldap
046 */
047public class LDAPConnector extends AbstractLDAPConnector implements Component
048{
049    /** The compoent role */
050    public static final String ROLE = LDAPConnector.class.getName();
051
052    private static final int __LEVEL_SIZE = 100;
053
054    /**
055     * Get the ldap attributes of a node
056     * @param ldapDatasourceId The ldap datasource id
057     * @param dn The dn of the ldap node to get
058     * @return The attributes
059     * @throws ProcessingException If an error occurred with ldap
060     */
061    public List<Map<String, String>> getAttributes(String ldapDatasourceId, String dn) throws ProcessingException
062    {
063        try
064        {
065            _delayedInitialize(ldapDatasourceId);
066        }
067        catch (Exception e)
068        {
069            throw new ProcessingException(e);
070        }
071        
072        LdapContext context = null;
073        NamingEnumeration<String> iDs = null;
074        try
075        {
076            // Connect to the LDAP server.
077            context = new InitialLdapContext(_getContextEnv(), null);
078            Attributes attributes = context.getAttributes(StringUtils.substringBefore(dn, "," + _ldapBaseDN));
079           
080            List<Map<String, String>> returnedAttributes = new ArrayList<>();
081            
082            iDs = attributes.getIDs();
083            while (iDs.hasMoreElements())
084            {
085                String attrId = iDs.nextElement();
086                Attribute attribute = attributes.get(attrId);
087                for (int i = 0; i < attribute.size(); i++)
088                {
089                    String valueAsString;
090                    
091                    try
092                    {
093                        Object value = attribute.get(i);
094                        if (value instanceof byte[])
095                        {
096                            byte[] valueAsBytes = (byte[]) value;
097                            valueAsString = new String(valueAsBytes, StandardCharsets.UTF_8);
098                        }
099                        else
100                        {
101                            valueAsString = value.toString();
102                        }
103                        
104                        if (valueAsString.length() > 255)
105                        {
106                            valueAsString = valueAsString.substring(0, 255) + "…";
107                        }
108                    }
109                    catch (Throwable t)
110                    {
111                        valueAsString = "Error retrieving: " + t.getMessage();
112                        getLogger().error("Cannot display value n°" + i + " of LDAP attribute '" + attrId + "' at " + (dn + "," + _ldapBaseDN), t);
113                    }
114                    
115                    Map<String, String> returnedAttribute = new HashMap<>();
116                    returnedAttribute.put("name", attrId);
117                    returnedAttribute.put("value", valueAsString);
118                    returnedAttributes.add(returnedAttribute);
119                }
120            }
121            
122            return returnedAttributes;
123        }
124        catch (NamingException e)
125        {
126            throw new ProcessingException(e);
127        }
128        finally
129        {
130            // Close connection resources
131            _cleanup(context, iDs);
132        }
133    }
134    
135    /**
136     * Get children DN
137     * @param ldapDatasourceId The ldap datasource id
138     * @param dn The parent DN. Can be empty to get root DN.
139     * @return The children DNs
140     * @throws ProcessingException If an exception occurred while reading the ldap
141     */ 
142    public Collection<DN> getChildren(String ldapDatasourceId, String dn) throws ProcessingException
143    {
144        try
145        {
146            _delayedInitialize(ldapDatasourceId);
147        }
148        catch (Exception e)
149        {
150            throw new ProcessingException(e);
151        }
152        
153        String filter = "(objectClass=*)";
154        SearchControls constraints = new SearchControls();
155        constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
156
157        try
158        {
159            if (StringUtils.isEmpty(dn))
160            {
161                int count = _count(__DEFAULT_PAGE_SIZE, "", filter, new Object[0], constraints);
162                return Collections.singletonList(new DN(_ldapBaseDN, _ldapBaseDN + (count > 0 ? " (" + count + ")" : ""), count > 0));
163            }
164            else
165            {
166                Collection<DN> childrenDN = new ArrayList<>();
167                
168                int offset = 0;
169                int limit = Integer.MAX_VALUE;
170                if (dn.startsWith("#"))
171                {
172                    // limited subpart
173                    String count = StringUtils.substringBefore(dn, ",").substring(1);
174                    offset = Integer.parseInt(StringUtils.substringBefore(count, "-")) - 1;
175                    limit = Integer.parseInt(StringUtils.substringAfter(count, "-")) - offset;
176                }
177                
178                // Remove the pseudo-nodes
179                String finalDN = Arrays.stream(StringUtils.split(dn, ",")).filter(s -> !s.startsWith("#")).collect(Collectors.joining(","));
180                
181                String subDN = finalDN.length() > _ldapBaseDN.length() ? finalDN.substring(0, finalDN.length() - (_ldapBaseDN.length() + 1)) : "";
182                String prefixedSubDN = subDN.length() > 0 ? "," + subDN : "";
183                
184                int nodeSize = limit < Integer.MAX_VALUE ? limit : _count(__DEFAULT_PAGE_SIZE, subDN, filter, new Object[0], constraints);
185                
186                final int levelNeeded = (int) Math.floor(Math.log10(nodeSize - 1) / Math.log10(__LEVEL_SIZE));
187                
188                if (levelNeeded <= 0)
189                {
190                    // a few entries are sent directly
191                    // TODO sort
192                    List<SearchResult> results = _search(__DEFAULT_PAGE_SIZE, subDN, filter, new Object[0], constraints, offset, limit);
193                    for (Iterator<SearchResult> iterator = results.iterator(); iterator.hasNext();)
194                    {
195                        SearchResult searchResult = iterator.next();
196                        int count = _count(__DEFAULT_PAGE_SIZE, searchResult.getName() + prefixedSubDN, filter, new Object[0], constraints);
197                        childrenDN.add(new DN(searchResult.getName(), searchResult.getName() + (count > 0 ? " (" + count + ")" : ""), count > 0));
198                    }
199                }
200                else
201                {
202                    final int step = (int) Math.pow(__LEVEL_SIZE, levelNeeded);
203                    int done = 0;
204                    while (done < nodeSize)
205                    {
206                        int grow = Math.min(step, nodeSize - done);
207                        childrenDN.add(new DN("#" + (done + 1) + "-" + (done + grow), "[" + (done + 1) + "..." + (done + grow) + "]", true));
208                        done += grow;
209                    }
210                }
211    
212                return childrenDN;
213            }
214        }
215        catch (NamingException e)
216        {
217            throw new ProcessingException(e);
218        }
219    }
220    
221    /**
222     * Count the number of results
223     * @param pageSize The number of entries in a page
224     * @param name  the name of the context or object to search
225     * @param filter the filter expression to use for the search
226     * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null.
227     * @param searchControls the search controls that control the search.
228     * @return The number of results
229     * @throws NamingException If an error occurred
230     */
231    protected boolean _hasResults(int pageSize, String name, String filter, Object[] filterArgs, SearchControls searchControls) throws NamingException
232    {
233        long countLimit = searchControls.getCountLimit();
234        searchControls.setCountLimit(1);
235        
236        try
237        {
238            return _count(pageSize, name, filter, filterArgs, searchControls) > 0;
239        }
240        finally
241        {
242            searchControls.setCountLimit(countLimit);
243        }
244    }
245    
246    /**
247     * Count the number of results
248     * @param pageSize The number of entries in a page
249     * @param name  the name of the context or object to search
250     * @param filter the filter expression to use for the search
251     * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null.
252     * @param searchControls the search controls that control the search.
253     * @return The number of results
254     * @throws NamingException If an error occurred
255     */
256    protected int _count(int pageSize, String name, String filter, Object[] filterArgs, SearchControls searchControls) throws NamingException
257    {
258        int count = 0;
259        
260        LdapContext context = null;
261        NamingEnumeration<SearchResult> tmpResults = null;
262        
263        boolean returningObjFlag = searchControls.getReturningObjFlag();
264        String[] attrs = searchControls.getReturningAttributes();
265        searchControls.setReturningObjFlag(false);
266        searchControls.setReturningAttributes(new String[0]);
267        
268        try
269        {
270            // Connect to the LDAP server.
271            context = new InitialLdapContext(_getContextEnv(), null);
272            
273            _setPagingIfSupported(pageSize, context);
274            
275            do
276            {
277                // Perform the search
278                tmpResults = context.search(name, filter, filterArgs, searchControls);
279                
280                // Iterate over a batch of search results
281                while (tmpResults != null && tmpResults.hasMoreElements())
282                {
283                    tmpResults.nextElement();
284                    
285                    count++;
286                }
287            }
288            while (_hasMoreEntries(pageSize, context));
289        }
290        finally
291        {
292            // Close connection resources
293            _cleanup(context, tmpResults);
294            searchControls.setReturningObjFlag(returningObjFlag);
295            searchControls.setReturningAttributes(attrs);
296        }
297        
298        return count;
299    }
300
301    /**
302     * A DN
303     */
304    public static class DN 
305    {
306        private String _dn;
307        private boolean _hasChild;
308        private String _label;
309        
310        /**
311         * Create a DN
312         * @param dn The ldap dn
313         * @param label The label for the dn
314         * @param hasChild Has this DN children
315         */
316        public DN(String dn, String label, boolean hasChild)
317        {
318            _dn = dn;
319            _label = label;
320            _hasChild = hasChild;
321        }
322        
323        /**
324         * Get the dn
325         * @return the dn
326         */
327        public String getDN()
328        {
329            return _dn;
330        }
331        
332        /**
333         * Get the label
334         * @return the label
335         */
336        public String getLabel()
337        {
338            return _label;
339        }
340        
341        /**
342         * Has child
343         * @return true if has child
344         */
345        public boolean hasChild()
346        {
347            return _hasChild;
348        }
349    }
350}