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}