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