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}