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.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.stream.Collectors;
027
028import javax.naming.Binding;
029import javax.naming.NameClassPair;
030import javax.naming.NamingEnumeration;
031import javax.naming.NamingException;
032import javax.naming.directory.Attribute;
033import javax.naming.directory.Attributes;
034import javax.naming.directory.SearchControls;
035import javax.naming.directory.SearchResult;
036import javax.naming.ldap.Control;
037import javax.naming.ldap.InitialLdapContext;
038import javax.naming.ldap.LdapContext;
039import javax.naming.ldap.PagedResultsControl;
040import javax.naming.ldap.PagedResultsResponseControl;
041
042import org.apache.avalon.framework.component.Component;
043import org.apache.cocoon.ProcessingException;
044import org.apache.commons.lang3.StringUtils;
045
046import org.ametys.core.util.ldap.AbstractLDAPConnector;
047
048/**
049 * Connection stuff to read ldap
050 */
051public class LDAPConnector extends AbstractLDAPConnector implements Component
052{
053    /** The compoent role */
054    public static final String ROLE = LDAPConnector.class.getName();
055
056    private static final int __LEVEL_SIZE = 100;
057
058    /**
059     * Get the ldap attributes of a node
060     * @param ldapDatasourceId The ldap datasource id
061     * @param dn The dn of the ldap node to get
062     * @return The attributes
063     * @throws ProcessingException If an error occurred with ldap
064     */
065    public List<Map<String, String>> getAttributes(String ldapDatasourceId, String dn) throws ProcessingException
066    {
067        try
068        {
069            _delayedInitialize(ldapDatasourceId);
070        }
071        catch (Exception e)
072        {
073            throw new ProcessingException(e);
074        }
075        
076        LdapContext context = null;
077        NamingEnumeration<String> iDs = null;
078        try
079        {
080            // Connect to the LDAP server.
081            context = new InitialLdapContext(_getContextEnv(), null);
082            Attributes attributes = context.getAttributes(StringUtils.substringBefore(dn, "," + _ldapBaseDN));
083           
084            List<Map<String, String>> returnedAttributes = new ArrayList<>();
085            
086            iDs = attributes.getIDs();
087            while (iDs.hasMoreElements())
088            {
089                String attrId = iDs.nextElement();
090                Attribute attribute = attributes.get(attrId);
091                for (int i = 0; i < attribute.size(); i++)
092                {
093                    String value = (String) attribute.get(i);
094                    
095                    Map<String, String> returnedAttribute = new HashMap<>();
096                    returnedAttribute.put("name", attrId);
097                    returnedAttribute.put("value", value);
098                    returnedAttributes.add(returnedAttribute);
099                }
100            }
101            
102            return returnedAttributes;
103        }
104        catch (NamingException e)
105        {
106            throw new ProcessingException(e);
107        }
108        finally
109        {
110            // Close connection resources
111            _cleanup(context, iDs);
112        }
113    }
114    
115    /**
116     * Get children DN
117     * @param ldapDatasourceId The ldap datasource id
118     * @param dn The parent DN. Can be empty to get root DN.
119     * @return The children DNs
120     * @throws ProcessingException If an exception occurred while reading the ldap
121     */ 
122    public Collection<DN> getChildren(String ldapDatasourceId, String dn) throws ProcessingException
123    {
124        try
125        {
126            _delayedInitialize(ldapDatasourceId);
127        }
128        catch (Exception e)
129        {
130            throw new ProcessingException(e);
131        }
132        
133        try
134        {
135            LdapContext context = null;
136            NamingEnumeration<Binding> listBindings = null;
137            try
138            {
139                // Connect to the LDAP server.
140                context = new InitialLdapContext(_getContextEnv(), null);
141                
142                if (StringUtils.isEmpty(dn))
143                {
144                    // Root node
145                    int count = _count(context);
146                    return Collections.singletonList(new DN(_ldapBaseDN, _ldapBaseDN + (count > 0 ? " (" + count + ")" : ""), count > 0));
147                }
148                else
149                {
150                    // Sub nodes
151                    Collection<DN> childrenDN = new ArrayList<>();
152                    int offset = 0;
153                    int limit = Integer.MAX_VALUE;
154                    if (dn.startsWith("#"))
155                    {
156                        // limited subpart
157                        String count = StringUtils.substringBefore(dn, ",").substring(1);
158                        offset = Integer.parseInt(StringUtils.substringBefore(count, "-")) - 1;
159                        limit = Integer.parseInt(StringUtils.substringAfter(count, "-")) - offset;
160                    }
161                    
162                    // Remove the pseudo-nodes
163                    String finalDN = Arrays.stream(StringUtils.split(dn, ",")).filter(s -> !s.startsWith("#")).collect(Collectors.joining(","));
164                    
165                    String subDN = finalDN.length() > _ldapBaseDN.length() ? finalDN.substring(0, finalDN.length() - (_ldapBaseDN.length() + 1)) : "";
166                    LdapContext subContext = (LdapContext) context.lookup(subDN);
167                    
168                    int nodeSize = limit < Integer.MAX_VALUE ? limit : _count(subContext);
169                    
170                    final int levelNeeded = (int) Math.floor(Math.log10(nodeSize - 1) / Math.log10(__LEVEL_SIZE));
171                    
172                    if (levelNeeded <= 0)
173                    {
174                        // a few entries are sent directly
175                        int index = 0;
176                        listBindings = subContext.listBindings("");
177                        while (listBindings.hasMore())
178                        {
179                            Binding next = listBindings.next();
180                            index++;
181                            if (index >= offset)
182                            {
183                                if (index < offset + limit)
184                                {
185                                    int count = _count((LdapContext) next.getObject());
186                                    childrenDN.add(new DN(next.getName(), next.getName() + (count > 0 ? " (" + count + ")" : ""), count > 0));
187                                }
188                                else
189                                {
190                                    break;
191                                }
192                            }
193                        }
194                    }
195                    else
196                    {
197                        final int step = (int) Math.pow(__LEVEL_SIZE, levelNeeded);
198                        int done = 0;
199                        while (done < nodeSize)
200                        {
201                            int grow = Math.min(step, nodeSize - done);
202                            childrenDN.add(new DN("#" + (done + 1) + "-" + (done + grow), "[" + (done + 1) + "..." + (done + grow) + "]", true));
203                            done += grow;
204                        }
205                    }
206        
207                    return childrenDN;
208                }
209            }
210            finally
211            {
212                // Close connection resources
213                _cleanup(context, listBindings);
214            }
215        }
216        catch (NamingException e)
217        {
218            throw new ProcessingException(e);
219        }
220    }
221    
222    /**
223     * Count the number of results
224     * @param pageSize The number of entries in a page
225     * @param name  the name of the context or object to search
226     * @param filter the filter expression to use for the search
227     * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null.
228     * @param searchControls the search controls that control the search.
229     * @return The number of results
230     * @throws NamingException If an error occurred
231     */
232    protected boolean _hasResults(int pageSize, String name, String filter, Object[] filterArgs, SearchControls searchControls) throws NamingException
233    {
234        long countLimit = searchControls.getCountLimit();
235        searchControls.setCountLimit(1);
236        
237        try
238        {
239            return _count(pageSize, name, filter, filterArgs, searchControls) > 0;
240        }
241        finally
242        {
243            searchControls.setCountLimit(countLimit);
244        }
245    }
246
247    /**
248     * Ccount the child node of a cocntext
249     * @param context The context
250     * @return The number of children
251     * @throws NamingException if an error occurred
252     */
253    protected int _count(LdapContext context) throws NamingException
254    {
255        int count = 0;
256        NamingEnumeration<NameClassPair> list = null;
257        try
258        {
259            list = context.list("");
260            while (list.hasMore())
261            {
262                list.next();
263                count++;
264            }
265        }
266        finally 
267        {
268            _cleanup(null, list);
269        }
270        return count;
271    }
272    
273    /**
274     * Count the number of results
275     * @param pageSize The number of entries in a page
276     * @param name  the name of the context or object to search
277     * @param filter the filter expression to use for the search
278     * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null.
279     * @param searchControls the search controls that control the search.
280     * @return The number of results
281     * @throws NamingException If an error occurred
282     */
283    protected int _count(int pageSize, String name, String filter, Object[] filterArgs, SearchControls searchControls) throws NamingException
284    {
285        int count = 0;
286        
287        LdapContext context = null;
288        NamingEnumeration<SearchResult> tmpResults = null;
289        
290        boolean returningObjFlag = searchControls.getReturningObjFlag();
291        String[] attrs = searchControls.getReturningAttributes();
292        searchControls.setReturningObjFlag(false);
293        searchControls.setReturningAttributes(new String[0]);
294        
295        try
296        {
297            // Connect to the LDAP server.
298            context = new InitialLdapContext(_getContextEnv(), null);
299            byte[] cookie = null;
300            
301            _setPagingIfSupported(pageSize, context);
302            
303            do
304            {
305                // Perform the search
306                tmpResults = context.search(name, filter, filterArgs, searchControls);
307                
308                // Iterate over a batch of search results
309                while (tmpResults != null && tmpResults.hasMoreElements())
310                {
311                    tmpResults.nextElement();
312                    
313                    count++;
314                    
315                    // Examine the paged results control response
316                    Control[] controls = context.getResponseControls();
317                    if (controls != null)
318                    {
319                        for (int i = 0; i < controls.length; i++)
320                        {
321                            if (controls[i] instanceof PagedResultsResponseControl)
322                            {
323                                PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
324                                cookie = prrc.getCookie();
325                            }
326                        }
327                    }
328                    
329                    // Re-activate paged results
330                    if (isPagingSupported())
331                    {
332                        try
333                        {
334                            context.setRequestControls(new Control[]{new PagedResultsControl(pageSize, cookie, Control.NONCRITICAL)});
335                        }
336                        catch (IOException ioe)
337                        {
338                            getLogger().error("Error setting the PagedResultsControl in the LDAP context.", ioe);
339                        }
340                    }
341                }
342            }
343            while (cookie != null);
344        }
345        finally
346        {
347            // Close connection resources
348            _cleanup(context, tmpResults);
349            searchControls.setReturningObjFlag(returningObjFlag);
350            searchControls.setReturningAttributes(attrs);
351        }
352        
353        return count;
354    }
355
356    /**
357     * A DN
358     */
359    public static class DN 
360    {
361        private String _dn;
362        private boolean _hasChild;
363        private String _label;
364        
365        /**
366         * Create a DN
367         * @param dn The ldap dn
368         * @param label The label for the dn
369         * @param hasChild Has this DN children
370         */
371        public DN(String dn, String label, boolean hasChild)
372        {
373            _dn = dn;
374            _label = label;
375            _hasChild = hasChild;
376        }
377        
378        /**
379         * Get the dn
380         * @return the dn
381         */
382        public String getDN()
383        {
384            return _dn;
385        }
386        
387        /**
388         * Get the label
389         * @return the label
390         */
391        public String getLabel()
392        {
393            return _label;
394        }
395        
396        /**
397         * Has child
398         * @return true if has child
399         */
400        public boolean hasChild()
401        {
402            return _hasChild;
403        }
404    }
405}