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