001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020package org.apache.directory.server.core.api.filtering; 021 022 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.List; 027 028import org.apache.directory.api.ldap.model.constants.Loggers; 029import org.apache.directory.api.ldap.model.cursor.AbstractCursor; 030import org.apache.directory.api.ldap.model.cursor.ClosureMonitor; 031import org.apache.directory.api.ldap.model.cursor.Cursor; 032import org.apache.directory.api.ldap.model.cursor.CursorException; 033import org.apache.directory.api.ldap.model.cursor.InvalidCursorPositionException; 034import org.apache.directory.api.ldap.model.entry.Entry; 035import org.apache.directory.api.ldap.model.exception.LdapException; 036import org.apache.directory.api.ldap.model.exception.OperationAbandonedException; 037import org.apache.directory.api.ldap.model.schema.SchemaManager; 038import org.apache.directory.server.core.api.entry.ClonedServerEntry; 039import org.apache.directory.server.core.api.entry.ServerEntryUtils; 040import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044 045/** 046 * A Cursor which uses a list of filters to selectively return entries and/or 047 * modify the contents of entries. Uses lazy pre-fetching on positioning 048 * operations which means adding filters after creation will not miss candidate 049 * entries. 050 * 051 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 052 */ 053public class EntryFilteringCursorImpl extends AbstractCursor<Entry> implements EntryFilteringCursor 054{ 055 /** the logger used by this class */ 056 private static final Logger LOG = LoggerFactory.getLogger( EntryFilteringCursorImpl.class ); 057 058 /** A dedicated log for cursors */ 059 private static final Logger LOG_CURSOR = LoggerFactory.getLogger( Loggers.CURSOR_LOG.getName() ); 060 061 /** Speedup for logs */ 062 private static final boolean IS_DEBUG = LOG_CURSOR.isDebugEnabled(); 063 064 /** the underlying wrapped search results Cursor */ 065 private final Cursor<Entry> wrapped; 066 067 /** the parameters associated with the search operation */ 068 private final SearchOperationContext operationContext; 069 070 /** The SchemaManager */ 071 private final SchemaManager schemaManager; 072 073 /** the list of filters to be applied */ 074 private final List<EntryFilter> filters; 075 076 /** the first accepted search result that is pre fetched */ 077 private Entry prefetched; 078 079 080 // ------------------------------------------------------------------------ 081 // C O N S T R U C T O R S 082 // ------------------------------------------------------------------------ 083 084 /** 085 * Creates a new entry filtering Cursor over an existing Cursor using a 086 * single filter initially: more can be added later after creation. 087 * 088 * @param wrapped the underlying wrapped Cursor whose entries are filtered 089 * @param schemaManager The SchemaManager instance 090 * @param operationContext The OperationContext instance 091 * @param filter a single filter to be used 092 */ 093 public EntryFilteringCursorImpl( Cursor<Entry> wrapped, 094 SearchOperationContext operationContext, SchemaManager schemaManager, EntryFilter filter ) 095 { 096 this( wrapped, operationContext, schemaManager, Collections.singletonList( filter ) ); 097 } 098 099 100 /** 101 * Creates a new entry filtering Cursor over an existing Cursor using a 102 * no filter initially: more can be added later after creation. 103 * 104 * @param wrapped the underlying wrapped Cursor whose entries are filtered 105 * @param operationContext The OperationContext instance 106 * @param schemaManager The SchemaManager instance 107 */ 108 public EntryFilteringCursorImpl( Cursor<Entry> wrapped, SearchOperationContext operationContext, 109 SchemaManager schemaManager ) 110 { 111 if ( IS_DEBUG ) 112 { 113 LOG_CURSOR.debug( "Creating BaseEntryFilteringCursor {}", this ); 114 } 115 116 this.wrapped = wrapped; 117 this.operationContext = operationContext; 118 this.filters = new ArrayList<>(); 119 this.schemaManager = schemaManager; 120 } 121 122 123 /** 124 * Creates a new entry filtering Cursor over an existing Cursor using a 125 * list of filters initially: more can be added later after creation. 126 * 127 * @param wrapped the underlying wrapped Cursor whose entries are filtered 128 * @param operationContext the operation context that created this Cursor 129 * @param schemaManager The SchemaManager instance 130 * @param filters a list of filters to be used 131 */ 132 public EntryFilteringCursorImpl( Cursor<Entry> wrapped, 133 SearchOperationContext operationContext, 134 SchemaManager schemaManager, 135 List<EntryFilter> filters ) 136 { 137 if ( IS_DEBUG ) 138 { 139 LOG_CURSOR.debug( "Creating BaseEntryFilteringCursor {}", this ); 140 } 141 142 this.wrapped = wrapped; 143 this.operationContext = operationContext; 144 this.filters = new ArrayList<>(); 145 this.filters.addAll( filters ); 146 this.schemaManager = schemaManager; 147 } 148 149 150 // ------------------------------------------------------------------------ 151 // Class Specific Methods 152 // ------------------------------------------------------------------------ 153 154 /* (non-Javadoc) 155 * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#isAbandoned() 156 */ 157 public boolean isAbandoned() 158 { 159 return operationContext.isAbandoned(); 160 } 161 162 163 /* (non-Javadoc) 164 * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#setAbandoned(boolean) 165 */ 166 public void setAbandoned( boolean abandoned ) 167 { 168 operationContext.setAbandoned( abandoned ); 169 170 if ( abandoned ) 171 { 172 LOG.info( "Cursor has been abandoned." ); 173 } 174 } 175 176 177 /* (non-Javadoc) 178 * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#addEntryFilter(org.apache.directory.server.core.filtering.EntryFilter) 179 */ 180 public boolean addEntryFilter( EntryFilter filter ) 181 { 182 return filters.add( filter ); 183 } 184 185 186 /* (non-Javadoc) 187 * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#removeEntryFilter(org.apache.directory.server.core.filtering.EntryFilter) 188 */ 189 public boolean removeEntryFilter( EntryFilter filter ) 190 { 191 return filters.remove( filter ); 192 } 193 194 195 /** 196 * {@inheritDoc} 197 */ 198 public List<EntryFilter> getEntryFilters() 199 { 200 return Collections.unmodifiableList( filters ); 201 } 202 203 204 /** 205 * {@inheritDoc} 206 */ 207 public SearchOperationContext getOperationContext() 208 { 209 return operationContext; 210 } 211 212 213 // ------------------------------------------------------------------------ 214 // Cursor Interface Methods 215 // ------------------------------------------------------------------------ 216 /** 217 * {@inheritDoc} 218 */ 219 public void after( Entry element ) throws LdapException, CursorException 220 { 221 throw new UnsupportedOperationException(); 222 } 223 224 225 /** 226 * {@inheritDoc} 227 */ 228 public void afterLast() throws LdapException, CursorException 229 { 230 wrapped.afterLast(); 231 prefetched = null; 232 } 233 234 235 /** 236 * {@inheritDoc} 237 */ 238 public boolean available() 239 { 240 return prefetched != null; 241 } 242 243 244 /** 245 * {@inheritDoc} 246 */ 247 public void before( Entry element ) throws LdapException, CursorException 248 { 249 throw new UnsupportedOperationException(); 250 } 251 252 253 /** 254 * {@inheritDoc} 255 */ 256 public void beforeFirst() throws LdapException, CursorException 257 { 258 wrapped.beforeFirst(); 259 prefetched = null; 260 } 261 262 263 /** 264 * {@inheritDoc} 265 */ 266 public void close() throws IOException 267 { 268 if ( IS_DEBUG ) 269 { 270 LOG_CURSOR.debug( "Closing BaseEntryFilteringCursor {}", this ); 271 } 272 273 wrapped.close(); 274 prefetched = null; 275 } 276 277 278 /** 279 * {@inheritDoc} 280 */ 281 public void close( Exception reason ) throws IOException 282 { 283 if ( IS_DEBUG ) 284 { 285 LOG_CURSOR.debug( "Closing BaseEntryFilteringCursor {}", this ); 286 } 287 288 wrapped.close( reason ); 289 prefetched = null; 290 } 291 292 293 /** 294 * {@inheritDoc} 295 */ 296 public final void setClosureMonitor( ClosureMonitor monitor ) 297 { 298 wrapped.setClosureMonitor( monitor ); 299 } 300 301 302 /** 303 * {@inheritDoc} 304 */ 305 public boolean first() throws LdapException, CursorException 306 { 307 if ( operationContext.isAbandoned() ) 308 { 309 LOG.info( "Cursor has been abandoned." ); 310 311 try 312 { 313 close(); 314 } 315 catch ( IOException ioe ) 316 { 317 throw new LdapException( ioe.getMessage(), ioe ); 318 } 319 320 throw new OperationAbandonedException(); 321 } 322 323 beforeFirst(); 324 325 return next(); 326 } 327 328 329 /** 330 * {@inheritDoc} 331 */ 332 public Entry get() throws InvalidCursorPositionException 333 { 334 if ( available() ) 335 { 336 return prefetched; 337 } 338 339 throw new InvalidCursorPositionException(); 340 } 341 342 343 /** 344 * {@inheritDoc} 345 */ 346 public boolean isClosed() 347 { 348 return wrapped.isClosed(); 349 } 350 351 352 /** 353 * {@inheritDoc} 354 */ 355 public boolean last() throws LdapException, CursorException 356 { 357 if ( operationContext.isAbandoned() ) 358 { 359 LOG.info( "Cursor has been abandoned." ); 360 361 try 362 { 363 close(); 364 } 365 catch ( IOException ioe ) 366 { 367 throw new LdapException( ioe.getMessage(), ioe ); 368 } 369 370 throw new OperationAbandonedException(); 371 } 372 373 afterLast(); 374 375 return previous(); 376 } 377 378 379 /** 380 * {@inheritDoc} 381 */ 382 public boolean next() throws LdapException, CursorException 383 { 384 if ( operationContext.isAbandoned() ) 385 { 386 LOG.info( "Cursor has been abandoned." ); 387 388 try 389 { 390 close(); 391 } 392 catch ( IOException ioe ) 393 { 394 throw new LdapException( ioe.getMessage(), ioe ); 395 } 396 397 throw new OperationAbandonedException(); 398 } 399 400 Entry tempResult = null; 401 402 outer: while ( wrapped.next() ) 403 { 404 Entry tempEntry = wrapped.get(); 405 406 if ( tempEntry == null ) 407 { 408 // no candidate 409 continue; 410 } 411 412 if ( tempEntry instanceof ClonedServerEntry ) 413 { 414 tempResult = tempEntry; 415 } 416 else 417 { 418 tempResult = new ClonedServerEntry( tempEntry ); 419 } 420 421 /* 422 * O P T I M I Z A T I O N 423 * ----------------------- 424 * 425 * Don't want to waste cycles on enabling a loop for processing 426 * filters if we have zero or one filter. 427 */ 428 429 if ( filters.isEmpty() ) 430 { 431 prefetched = tempResult; 432 ServerEntryUtils.filterContents( 433 schemaManager, 434 operationContext, prefetched ); 435 436 return true; 437 } 438 439 if ( ( filters.size() == 1 ) && filters.get( 0 ).accept( operationContext, tempResult ) ) 440 { 441 prefetched = tempResult; 442 ServerEntryUtils.filterContents( 443 schemaManager, 444 operationContext, prefetched ); 445 446 return true; 447 } 448 449 /* E N D O P T I M I Z A T I O N */ 450 for ( EntryFilter filter : filters ) 451 { 452 // if a filter rejects then short and continue with outer loop 453 if ( !filter.accept( operationContext, tempResult ) ) 454 { 455 continue outer; 456 } 457 } 458 459 /* 460 * Here the entry has been accepted by all filters. 461 */ 462 prefetched = tempResult; 463 464 return true; 465 } 466 467 prefetched = null; 468 469 return false; 470 } 471 472 473 /** 474 * {@inheritDoc} 475 */ 476 public boolean previous() throws LdapException, CursorException 477 { 478 if ( operationContext.isAbandoned() ) 479 { 480 LOG.info( "Cursor has been abandoned." ); 481 482 try 483 { 484 close(); 485 } 486 catch ( IOException ioe ) 487 { 488 throw new LdapException( ioe.getMessage(), ioe ); 489 } 490 491 throw new OperationAbandonedException(); 492 } 493 494 Entry tempResult = null; 495 496 outer: while ( wrapped.previous() ) 497 { 498 Entry entry = wrapped.get(); 499 500 if ( entry == null ) 501 { 502 continue; 503 } 504 505 tempResult = new ClonedServerEntry/*Search*/( entry ); 506 507 /* 508 * O P T I M I Z A T I O N 509 * ----------------------- 510 * 511 * Don't want to waste cycles on enabling a loop for processing 512 * filters if we have zero or one filter. 513 */ 514 515 if ( filters.isEmpty() ) 516 { 517 prefetched = tempResult; 518 ServerEntryUtils.filterContents( 519 schemaManager, 520 operationContext, prefetched ); 521 522 return true; 523 } 524 525 if ( ( filters.size() == 1 ) && filters.get( 0 ).accept( operationContext, tempResult ) ) 526 { 527 prefetched = tempResult; 528 ServerEntryUtils.filterContents( 529 schemaManager, 530 operationContext, prefetched ); 531 532 return true; 533 } 534 535 /* E N D O P T I M I Z A T I O N */ 536 537 for ( EntryFilter filter : filters ) 538 { 539 // if a filter rejects then short and continue with outer loop 540 if ( !filter.accept( operationContext, tempResult ) ) 541 { 542 continue outer; 543 } 544 } 545 546 /* 547 * Here the entry has been accepted by all filters. 548 */ 549 prefetched = tempResult; 550 ServerEntryUtils.filterContents( 551 schemaManager, 552 operationContext, prefetched ); 553 554 return true; 555 } 556 557 prefetched = null; 558 559 return false; 560 } 561 562 563 /** 564 * @see Object#toString() 565 */ 566 public String toString( String tabs ) 567 { 568 StringBuilder sb = new StringBuilder(); 569 570 if ( wrapped != null ) 571 { 572 sb.append( tabs ).append( "BaseEntryFilteringCursor, wrapped : \n" ); 573 sb.append( wrapped.toString( tabs + " " ) ); 574 } 575 else 576 { 577 sb.append( tabs ).append( "BaseEntryFilteringCursor, no wrapped\n" ); 578 } 579 580 if ( ( filters != null ) && !filters.isEmpty() ) 581 { 582 sb.append( tabs ).append( "Filters : \n" ); 583 584 for ( EntryFilter filter : filters ) 585 { 586 sb.append( filter.toString( tabs + " " ) ).append( "\n" ); 587 } 588 } 589 else 590 { 591 sb.append( tabs ).append( "No filter\n" ); 592 } 593 594 if ( prefetched != null ) 595 { 596 sb.append( tabs ).append( "Prefetched : \n" ); 597 sb.append( prefetched.toString( tabs + " " ) ); 598 } 599 else 600 { 601 sb.append( tabs ).append( "No prefetched" ); 602 } 603 604 return sb.toString(); 605 } 606 607 608 /** 609 * @see Object#toString() 610 */ 611 public String toString() 612 { 613 return toString( "" ); 614 } 615}