001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008 Sun Microsystems, Inc. 025 * Portions Copyright 2014-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import static org.opends.server.util.CollectionUtils.*; 030 031import java.util.Iterator; 032import java.util.LinkedHashMap; 033import java.util.LinkedHashSet; 034import java.util.LinkedList; 035import java.util.Set; 036import java.util.concurrent.LinkedBlockingQueue; 037import java.util.concurrent.TimeUnit; 038 039import org.forgerock.opendj.ldap.SearchScope; 040import org.opends.server.types.DN; 041import org.opends.server.types.DirectoryException; 042import org.opends.server.types.Entry; 043import org.opends.server.types.LDAPURL; 044import org.opends.server.types.MemberList; 045import org.opends.server.types.MembershipException; 046import org.opends.server.types.SearchFilter; 047 048/** 049 * This class defines a mechanism that may be used to iterate over the 050 * members of a dynamic group, optionally using an additional set of 051 * criteria to further filter the results. 052 */ 053public class DynamicGroupMemberList 054 extends MemberList 055{ 056 /** Indicates whether the search thread has completed its processing. */ 057 private boolean searchesCompleted; 058 059 /** The base DN to use when filtering the set of group members. */ 060 private final DN baseDN; 061 062 /** The DN of the entry containing the group definition. */ 063 private final DN groupDN; 064 065 /** 066 * The queue into which results will be placed while they are waiting to be 067 * returned. The types of objects that may be placed in this queue are Entry 068 * objects to return or MembershipException objects to throw. 069 */ 070 private final LinkedBlockingQueue<Object> resultQueue; 071 072 /** The search filter to use when filtering the set of group members. */ 073 private final SearchFilter filter; 074 075 /** The search scope to use when filtering the set of group members. */ 076 private final SearchScope scope; 077 078 /** The set of LDAP URLs that define the membership criteria. */ 079 private final Set<LDAPURL> memberURLs; 080 081 082 083 /** 084 * Creates a new dynamic group member list with the provided information. 085 * 086 * @param groupDN The DN of the entry containing the group definition. 087 * @param memberURLs The set of LDAP URLs that define the membership 088 * criteria for the associated group. 089 * 090 * @throws DirectoryException If a problem occurs while creating the member 091 * list. 092 */ 093 public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs) 094 throws DirectoryException 095 { 096 this(groupDN, memberURLs, null, null, null); 097 } 098 099 100 101 /** 102 * Creates a new dynamic group member list with the provided information. 103 * 104 * @param groupDN The DN of the entry containing the group definition. 105 * @param memberURLs The set of LDAP URLs that define the membership 106 * criteria for the associated group. 107 * @param baseDN The base DN that should be enforced for all entries to 108 * return. 109 * @param scope The scope that should be enforced for all entries to 110 * return. 111 * @param filter The filter that should be enforced for all entries to 112 * return. 113 * 114 * @throws DirectoryException If a problem occurs while creating the member 115 * list. 116 */ 117 public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs, 118 DN baseDN, SearchScope scope, 119 SearchFilter filter) 120 throws DirectoryException 121 { 122 this.groupDN = groupDN; 123 this.memberURLs = memberURLs; 124 this.baseDN = baseDN; 125 this.filter = filter; 126 127 if (scope == null) 128 { 129 this.scope = SearchScope.WHOLE_SUBTREE; 130 } 131 else 132 { 133 this.scope = scope; 134 } 135 136 searchesCompleted = false; 137 resultQueue = new LinkedBlockingQueue<>(10); 138 139 140 // We're going to have to perform one or more internal searches in order to 141 // get the results. We need to be careful about the way that we construct 142 // them in order to avoid the possibility of getting duplicate results, so 143 // searches with overlapping bases will need to be combined. 144 LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs = new LinkedHashMap<>(); 145 for (LDAPURL memberURL : memberURLs) 146 { 147 // First, determine the base DN for the search. It needs to be evaluated 148 // as relative to both the overall base DN specified in the set of 149 // criteria, as well as any other existing base DNs in the same hierarchy. 150 DN urlBaseDN = memberURL.getBaseDN(); 151 if (baseDN != null) 152 { 153 if (baseDN.isDescendantOf(urlBaseDN)) 154 { 155 // The base DN requested by the user is below the base DN for this 156 // URL, so we'll use the base DN requested by the user. 157 urlBaseDN = baseDN; 158 } 159 else if (! urlBaseDN.isDescendantOf(baseDN)) 160 { 161 // The base DN from the URL is outside the base requested by the user, 162 // so we can skip this URL altogether. 163 continue; 164 } 165 } 166 167 // If this is the first URL, then we can just add it with the base DN. 168 // Otherwise, we need to see if it needs to be merged with other URLs in 169 // the same hierarchy. 170 if (baseDNs.isEmpty()) 171 { 172 baseDNs.put(urlBaseDN, newLinkedList(memberURL)); 173 } 174 else 175 { 176 // See if the specified base DN is already in the map. If so, then 177 // just add the new URL to the existing list. 178 LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN); 179 if (urlList == null) 180 { 181 // There's no existing list for the same base DN, but there might be 182 // DNs in an overlapping hierarchy. If so, then use the base DN that 183 // is closest to the naming context. If not, then add a new list with 184 // the current base DN. 185 boolean found = false; 186 Iterator<DN> iterator = baseDNs.keySet().iterator(); 187 while (iterator.hasNext()) 188 { 189 DN existingBaseDN = iterator.next(); 190 if (urlBaseDN.isDescendantOf(existingBaseDN)) 191 { 192 // The base DN for the current URL is below an existing base DN, 193 // so we can just add this URL to the existing list and be done. 194 urlList = baseDNs.get(existingBaseDN); 195 urlList.add(memberURL); 196 found = true; 197 break; 198 } 199 else if (existingBaseDN.isDescendantOf(urlBaseDN)) 200 { 201 // The base DN for the current URL is above the existing base DN, 202 // so we should use the base DN for the current URL instead of the 203 // existing one. 204 urlList = baseDNs.get(existingBaseDN); 205 urlList.add(memberURL); 206 iterator.remove(); 207 baseDNs.put(urlBaseDN, urlList); 208 found = true; 209 break; 210 } 211 } 212 213 if (! found) 214 { 215 baseDNs.put(urlBaseDN, newLinkedList(memberURL)); 216 } 217 } 218 else 219 { 220 // There was already a list with the same base DN, so just add the URL. 221 urlList.add(memberURL); 222 } 223 } 224 } 225 226 227 // At this point, we should know what base DN(s) we need to use, so we can 228 // create the filter to use with that base DN. There are some special-case 229 // optimizations that we can do here, but in general the filter will look 230 // like "(&(filter)(|(urlFilters)))". 231 LinkedHashMap<DN,SearchFilter> searchMap = new LinkedHashMap<>(); 232 for (DN urlBaseDN : baseDNs.keySet()) 233 { 234 LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN); 235 LinkedHashSet<SearchFilter> urlFilters = new LinkedHashSet<>(); 236 for (LDAPURL url : urlList) 237 { 238 urlFilters.add(url.getFilter()); 239 } 240 241 SearchFilter combinedFilter; 242 if (filter == null) 243 { 244 if (urlFilters.size() == 1) 245 { 246 combinedFilter = urlFilters.iterator().next(); 247 } 248 else 249 { 250 combinedFilter = SearchFilter.createORFilter(urlFilters); 251 } 252 } 253 else 254 { 255 if (urlFilters.size() == 1) 256 { 257 SearchFilter urlFilter = urlFilters.iterator().next(); 258 if (urlFilter.equals(filter)) 259 { 260 combinedFilter = filter; 261 } 262 else 263 { 264 LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>(); 265 filterSet.add(filter); 266 filterSet.add(urlFilter); 267 combinedFilter = SearchFilter.createANDFilter(filterSet); 268 } 269 } 270 else 271 { 272 if (urlFilters.contains(filter)) 273 { 274 combinedFilter = filter; 275 } 276 else 277 { 278 LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>(); 279 filterSet.add(filter); 280 filterSet.add(SearchFilter.createORFilter(urlFilters)); 281 combinedFilter = SearchFilter.createANDFilter(filterSet); 282 } 283 } 284 } 285 286 searchMap.put(urlBaseDN, combinedFilter); 287 } 288 289 290 // At this point, we should have all the information we need to perform the 291 // searches. Create arrays of the elements for each. 292 DN[] baseDNArray = new DN[baseDNs.size()]; 293 SearchFilter[] filterArray = new SearchFilter[baseDNArray.length]; 294 LDAPURL[][] urlArray = new LDAPURL[baseDNArray.length][]; 295 Iterator<DN> iterator = baseDNs.keySet().iterator(); 296 for (int i=0; i < baseDNArray.length; i++) 297 { 298 baseDNArray[i] = iterator.next(); 299 filterArray[i] = searchMap.get(baseDNArray[i]); 300 301 LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]); 302 urlArray[i] = new LDAPURL[urlList.size()]; 303 int j=0; 304 for (LDAPURL url : urlList) 305 { 306 urlArray[i][j++] = url; 307 } 308 } 309 310 311 DynamicGroupSearchThread searchThread = 312 new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray); 313 searchThread.start(); 314 } 315 316 317 318 /** 319 * Retrieves the DN of the dynamic group with which this dynamic group member 320 * list is associated. 321 * 322 * @return The DN of the dynamic group with which this dynamic group member 323 * list is associated. 324 */ 325 public final DN getDynamicGroupDN() 326 { 327 return groupDN; 328 } 329 330 331 332 /** 333 * Indicates that all of the searches needed to iterate across the member list 334 * have completed and there will not be any more results provided. 335 */ 336 final void setSearchesCompleted() 337 { 338 searchesCompleted = true; 339 } 340 341 342 343 /** 344 * Adds the provided entry to the set of results that should be returned for 345 * this member list. 346 * 347 * @param entry The entry to add to the set of results that should be 348 * returned for this member list. 349 * 350 * @return {@code true} if the entry was added to the result set, or 351 * {@code false} if it was not (either because a timeout expired or 352 * the attempt was interrupted). If this method returns 353 * {@code false}, then the search thread should terminate 354 * immediately. 355 */ 356 final boolean addResult(Entry entry) 357 { 358 try 359 { 360 return resultQueue.offer(entry, 10, TimeUnit.SECONDS); 361 } 362 catch (InterruptedException ie) 363 { 364 return false; 365 } 366 } 367 368 369 370 /** 371 * Adds the provided membership exception so that it will be thrown along with 372 * the set of results for this member list. 373 * 374 * @param membershipException The membership exception to be thrown. 375 * 376 * @return {@code true} if the exception was added to the result set, or 377 * {@code false} if it was not (either because a timeout expired or 378 * the attempt was interrupted). If this method returns 379 * {@code false}, then the search thread should terminate 380 * immediately. 381 */ 382 final boolean addResult(MembershipException membershipException) 383 { 384 try 385 { 386 return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS); 387 } 388 catch (InterruptedException ie) 389 { 390 return false; 391 } 392 } 393 394 395 396 /** {@inheritDoc} */ 397 @Override 398 public boolean hasMoreMembers() 399 { 400 while (! searchesCompleted) 401 { 402 if (resultQueue.peek() != null) 403 { 404 return true; 405 } 406 407 try 408 { 409 Thread.sleep(0, 1000); 410 } catch (Exception e) {} 411 } 412 413 return resultQueue.peek() != null; 414 } 415 416 417 418 /** {@inheritDoc} */ 419 @Override 420 public Entry nextMemberEntry() 421 throws MembershipException 422 { 423 if (! hasMoreMembers()) 424 { 425 return null; 426 } 427 428 Object result = resultQueue.poll(); 429 if (result == null) 430 { 431 close(); 432 return null; 433 } 434 else if (result instanceof Entry) 435 { 436 return (Entry) result; 437 } 438 else if (result instanceof MembershipException) 439 { 440 MembershipException me = (MembershipException) result; 441 if (! me.continueIterating()) 442 { 443 close(); 444 } 445 446 throw me; 447 } 448 449 // We should never get here. 450 close(); 451 return null; 452 } 453 454 455 456 /** {@inheritDoc} */ 457 @Override 458 public void close() 459 { 460 searchesCompleted = true; 461 resultQueue.clear(); 462 } 463} 464