1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.server.operations.search;
21  
22  
23  import java.util.ArrayList;
24  import java.util.EventObject;
25  import java.util.List;
26  
27  import javax.naming.NamingEnumeration;
28  import javax.naming.directory.Attribute;
29  import javax.naming.directory.Attributes;
30  import javax.naming.directory.BasicAttribute;
31  import javax.naming.directory.BasicAttributes;
32  import javax.naming.directory.DirContext;
33  import javax.naming.directory.SearchResult;
34  import javax.naming.event.EventDirContext;
35  import javax.naming.event.NamespaceChangeListener;
36  import javax.naming.event.NamingEvent;
37  import javax.naming.event.NamingExceptionEvent;
38  import javax.naming.event.ObjectChangeListener;
39  import javax.naming.ldap.Control;
40  import javax.naming.ldap.HasControls;
41  import javax.naming.ldap.LdapContext;
42  
43  import org.apache.directory.server.core.event.EventService;
44  import org.apache.directory.server.core.event.RegistrationEntry;
45  import org.apache.directory.server.core.integ.Level;
46  import org.apache.directory.server.core.integ.annotations.ApplyLdifs;
47  import org.apache.directory.server.core.integ.annotations.CleanupLevel;
48  import org.apache.directory.server.integ.SiRunner;
49  import static org.apache.directory.server.integ.ServerIntegrationUtils.getWiredContext;
50  
51  import org.apache.directory.server.ldap.LdapService;
52  import org.apache.directory.shared.ldap.codec.search.controls.ChangeType;
53  import org.apache.directory.shared.ldap.codec.search.controls.EntryChangeControlCodec;
54  import org.apache.directory.shared.ldap.codec.search.controls.EntryChangeControlDecoder;
55  import org.apache.directory.shared.ldap.message.PersistentSearchControl;
56  
57  import org.junit.Test;
58  import org.junit.runner.RunWith;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import static org.junit.Assert.assertNull;
63  import static org.junit.Assert.assertEquals;
64  import static org.junit.Assert.assertNotNull;
65  import static org.junit.Assert.assertTrue;
66  
67  
68  /**
69   * Test case which tests the correct operation of the persistent search control.
70   * 
71   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
72   * @version $Rev: 692919 $
73   */
74  @RunWith ( SiRunner.class ) 
75  @CleanupLevel ( Level.SUITE )
76  @ApplyLdifs( {
77      // Entry # 2
78      "dn: cn=Tori Amos,ou=system\n" +
79      "objectClass: person\n" +
80      "objectClass: top\n" +
81      "cn: Tori Amos\n" +
82      "description: an American singer-songwriter\n" +
83      "sn: Amos\n\n"
84      }
85  )
86  public class PersistentSearchIT
87  {
88      private static final Logger LOG = LoggerFactory.getLogger( PersistentSearchIT.class );
89      
90      private static final String BASE = "ou=system";
91      private static final String PERSON_DESCRIPTION = "an American singer-songwriter";
92      private static final String RDN = "cn=Tori Amos";
93  
94      public static LdapService ldapService;
95      
96  
97      /**
98       * Creation of required attributes of a person entry.
99       */
100     protected Attributes getPersonAttributes( String sn, String cn )
101     {
102         Attributes attributes = new BasicAttributes( true );
103         Attribute attribute = new BasicAttribute( "objectClass" );
104         attribute.add( "top" );
105         attribute.add( "person" );
106         attributes.put( attribute );
107         attributes.put( "cn", cn );
108         attributes.put( "sn", sn );
109 
110         return attributes;
111     }
112 
113     
114     EventDirContext ctx;
115     EventService eventService; 
116     PSearchListener listener;
117     Thread t;
118     
119 
120     public void setUpListenerReturnECs() throws Exception
121     {
122         setUpListener( true, new PersistentSearchControl(), false );
123     }
124     
125     
126     public void setUpListener( boolean returnECs, PersistentSearchControl control, boolean ignoreEmptyRegistryCheck ) 
127         throws Exception
128     {
129         ctx = ( EventDirContext ) getWiredContext( ldapService).lookup( BASE );
130         eventService = ldapService.getDirectoryService().getEventService();
131         List<RegistrationEntry> registrationEntryList = eventService.getRegistrationEntries();
132         
133         if ( ! ignoreEmptyRegistryCheck )
134         {
135             assertTrue( registrationEntryList.isEmpty() );
136         }
137         
138         control.setReturnECs( returnECs );
139         listener = new PSearchListener( control );
140         t = new Thread( listener, "PSearchListener" );
141         t.start();
142 
143         // let's wait until the listener thread started
144         while ( eventService.getRegistrationEntries().isEmpty() )
145         {
146             Thread.sleep( 100 );
147         }
148         // Now we wait until the listener is registered (timing dependent crap)
149         Thread.sleep( 250 );
150     }
151     
152     
153     public void setUpListener() throws Exception
154     {
155         ctx = ( EventDirContext ) getWiredContext( ldapService).lookup( BASE );
156         eventService = ldapService.getDirectoryService().getEventService();
157         List<RegistrationEntry> registrationEntryList = eventService.getRegistrationEntries();
158         assertTrue( registrationEntryList.isEmpty() );
159         
160         listener = new PSearchListener();
161         t = new Thread( listener, "PSearchListener" );
162         t.start();
163 
164         // let's wait until the listener thread started
165         while ( eventService.getRegistrationEntries().isEmpty() )
166         {
167             Thread.sleep( 100 );
168         }
169         // Now we wait until the listener is registered (timing dependent crap)
170         Thread.sleep( 250 );
171     }
172     
173     
174     public void tearDownListener() throws Exception
175     {
176         listener.close();
177         ctx.close();
178 
179         while ( ! eventService.getRegistrationEntries().isEmpty() )
180         {
181             Thread.sleep( 100 );
182         }
183     }
184 
185     
186     private void waitForThreadToDie( Thread t ) throws Exception
187     {
188         long start = System.currentTimeMillis();
189         while ( t.isAlive() )
190         {
191             Thread.sleep( 200 );
192             if ( System.currentTimeMillis() - start > 1000 )
193             {
194                 break;
195             }
196         }
197     }
198 
199     
200     /**
201      * Shows correct notifications for modify(4) changes.
202      */
203     @Test
204     public void testPsearchModify() throws Exception
205     {
206         setUpListener();
207         ctx.modifyAttributes( RDN, DirContext.REMOVE_ATTRIBUTE, 
208             new BasicAttributes( "description", PERSON_DESCRIPTION, true ) );
209         waitForThreadToDie( t );
210         assertNotNull( listener.result );
211         assertEquals( RDN, listener.result.getName() );
212         tearDownListener();
213     }
214 
215 
216     /**
217      * Shows correct notifications for moddn(8) changes.
218      */
219     @Test
220     public void testPsearchModifyDn() throws Exception
221     {
222         setUpListener();
223         ctx.rename( RDN, "cn=Jack Black" );
224         waitForThreadToDie( t );
225         assertNotNull( listener.result );
226         assertEquals( "cn=Jack Black", listener.result.getName() );
227         tearDownListener();
228     }
229 
230 
231     /**
232      * Shows correct notifications for delete(2) changes.
233      */
234     @Test
235     public void testPsearchDelete() throws Exception
236     {
237         setUpListener();
238         ctx.destroySubcontext( RDN );
239         waitForThreadToDie( t );
240         assertNotNull( listener.result );
241         assertEquals( RDN, listener.result.getName() );
242         tearDownListener();
243     }
244 
245 
246     /**
247      * Shows correct notifications for add(1) changes.
248      */
249     @Test
250     public void testPsearchAdd() throws Exception
251     {
252         setUpListener();
253         ctx.createSubcontext( "cn=Jack Black", getPersonAttributes( "Black", "Jack Black" ) );
254         waitForThreadToDie( t );
255         assertNotNull( listener.result );
256         assertEquals( "cn=Jack Black", listener.result.getName() );
257         tearDownListener();
258     }
259 
260 
261     /**
262      * Shows correct notifications for modify(4) changes with returned 
263      * EntryChangeControl.
264      */
265     @Test
266     public void testPsearchModifyWithEC() throws Exception
267     {
268         setUpListenerReturnECs();
269         ctx.modifyAttributes( RDN, DirContext.REMOVE_ATTRIBUTE, new BasicAttributes( "description", PERSON_DESCRIPTION,
270             true ) );
271         waitForThreadToDie( t );
272         assertNotNull( listener.result );
273         assertEquals( RDN, listener.result.getName() );
274         assertEquals( listener.result.control.getChangeType(), ChangeType.MODIFY );
275         tearDownListener();
276     }
277 
278 
279     /**
280      * Shows correct notifications for moddn(8) changes with returned 
281      * EntryChangeControl.
282      */
283     @Test
284     public void testPsearchModifyDnWithEC() throws Exception
285     {
286         setUpListenerReturnECs();
287         ctx.rename( RDN, "cn=Jack Black" );
288         waitForThreadToDie( t );
289         assertNotNull( listener.result );
290         assertEquals( "cn=Jack Black", listener.result.getName() );
291         assertEquals( listener.result.control.getChangeType(), ChangeType.MODDN );
292         assertEquals( ( RDN + ",ou=system" ), listener.result.control.getPreviousDn().getUpName() );
293         tearDownListener();
294     }
295 
296 
297     /**
298      * Shows correct notifications for delete(2) changes with returned 
299      * EntryChangeControl.
300      */
301     @Test
302     public void testPsearchDeleteWithEC() throws Exception
303     {
304         setUpListenerReturnECs();
305         ctx.destroySubcontext( RDN );
306         waitForThreadToDie( t );
307         assertNotNull( listener.result );
308         assertEquals( RDN, listener.result.getName() );
309         assertEquals( listener.result.control.getChangeType(), ChangeType.DELETE );
310         tearDownListener();
311     }
312 
313 
314     /**
315      * Shows correct notifications for add(1) changes with returned 
316      * EntryChangeControl.
317      */
318     @Test
319     public void testPsearchAddWithEC() throws Exception
320     {
321         setUpListenerReturnECs();
322         ctx.createSubcontext( "cn=Jack Black", getPersonAttributes( "Black", "Jack Black" ) );
323         waitForThreadToDie( t );
324         assertNotNull( listener.result );
325         assertEquals( "cn=Jack Black", listener.result.getName() );
326         assertEquals( listener.result.control.getChangeType(), ChangeType.ADD );
327         tearDownListener();
328     }
329 
330 
331     /**
332      * Shows correct notifications for only add(1) and modify(4) registered changes with returned 
333      * EntryChangeControl but not deletes.
334      */
335     @Test
336     public void testPsearchAddModifyEnabledWithEC() throws Exception
337     {
338         PersistentSearchControl control = new PersistentSearchControl();
339         control.setReturnECs( true );
340         control.setChangeTypes( ChangeType.ADD_VALUE );
341         control.enableNotification( ChangeType.MODIFY );
342         setUpListener( true, control, false );
343         ctx.createSubcontext( "cn=Jack Black", getPersonAttributes( "Black", "Jack Black" ) );
344         waitForThreadToDie( t );
345 
346         assertNotNull( listener.result );
347         assertEquals( "cn=Jack Black", listener.result.getName() );
348         assertEquals( listener.result.control.getChangeType(), ChangeType.ADD );
349         tearDownListener();
350 
351         setUpListener( true, control, true );
352         ctx.destroySubcontext( "cn=Jack Black" );
353         waitForThreadToDie( t );
354         assertNull( listener.result );
355 
356         // thread is still waiting for notifications try a modify
357         ctx.modifyAttributes( RDN, DirContext.REMOVE_ATTRIBUTE, new BasicAttributes( "description", PERSON_DESCRIPTION,
358             true ) );
359         waitForThreadToDie( t );
360         
361         assertNotNull( listener.result );
362         assertEquals( RDN, listener.result.getName() );
363         assertEquals( listener.result.control.getChangeType(), ChangeType.MODIFY );
364         
365         tearDownListener();
366     }
367 
368 
369     /**
370      * Shows correct notifications for add(1) changes with returned 
371      * EntryChangeControl and changesOnly set to false so we return
372      * the first set of entries.
373      * 
374      * This test is commented out because it exhibits some producer
375      * consumer lockups (server and client being in same process)
376      * 
377      * PLUS ALL THIS GARBAGE IS TIME DEPENDENT!!!!!
378      */
379     //    public void testPsearchAddWithECAndFalseChangesOnly() throws Exception
380     //    {
381     //        PersistentSearchControl control = new PersistentSearchControl();
382     //        control.setReturnECs( true );
383     //        control.setChangesOnly( false );
384     //        PSearchListener listener = new PSearchListener( control );
385     //        Thread t = new Thread( listener );
386     //        t.start();
387     //        
388     //        Thread.sleep( 3000 );
389     //
390     //        assertEquals( 5, listener.count );
391     //        ctx.createSubcontext( "cn=Jack Black", getPersonAttributes( "Black", "Jack Black" ) );
392     //        
393     //        long start = System.currentTimeMillis();
394     //        while ( t.isAlive() )
395     //        {
396     //            Thread.sleep( 100 );
397     //            if ( System.currentTimeMillis() - start > 3000 )
398     //            {
399     //                break;
400     //            }
401     //        }
402     //        
403     //        assertEquals( 6, listener.count );
404     //        assertNotNull( listener.result );
405     //        assertEquals( "cn=Jack Black", listener.result.getName() );
406     //        assertEquals( listener.result.control.getChangeType(), ChangeType.ADD );
407     //    }
408 
409     /**
410      * Shows notifications functioning with the JNDI notification API of the SUN
411      * provider.
412      *
413     @Test
414     public void testPsearchAbandon() throws Exception
415     {
416         PersistentSearchControl control = new PersistentSearchControl();
417         control.setReturnECs( true );
418         PSearchListener listener = new PSearchListener( control );
419         Thread t = new Thread( listener );
420         t.start();
421 
422         while ( !listener.isReady )
423         {
424             Thread.sleep( 100 );
425         }
426         Thread.sleep( 250 );
427 
428         ctx.createSubcontext( "cn=Jack Black", getPersonAttributes( "Black", "Jack Black" ) );
429 
430         long start = System.currentTimeMillis();
431         while ( t.isAlive() )
432         {
433             Thread.sleep( 100 );
434             if ( System.currentTimeMillis() - start > 3000 )
435             {
436                 break;
437             }
438         }
439 
440         assertNotNull( listener.result );
441         assertEquals( "cn=Jack Black", listener.result.getName() );
442         assertEquals( listener.result.control.getChangeType(), ChangeType.ADD );
443         
444         listener = new PSearchListener( control );
445 
446         t = new Thread( listener );
447         t.start();
448 
449         ctx.destroySubcontext( "cn=Jack Black" );
450 
451         start = System.currentTimeMillis();
452         while ( t.isAlive() )
453         {
454             Thread.sleep( 100 );
455             if ( System.currentTimeMillis() - start > 3000 )
456             {
457                 break;
458             }
459         }
460 
461         // there seems to be a race condition here
462         // assertNull( listener.result );
463         assertNotNull( listener.result );
464         assertEquals( "cn=Jack Black", listener.result.getName() );
465         assertEquals( ChangeType.DELETE, listener.result.control.getChangeType() );
466         listener.result = null;
467 
468         // thread is still waiting for notifications try a modify
469         ctx.modifyAttributes( RDN, DirContext.REMOVE_ATTRIBUTE, new AttributesImpl( "description", PERSON_DESCRIPTION,
470             true ) );
471         start = System.currentTimeMillis();
472         while ( t.isAlive() )
473         {
474             Thread.sleep( 200 );
475             if ( System.currentTimeMillis() - start > 3000 )
476             {
477                 break;
478             }
479         }
480 
481         assertNull( listener.result );
482         //assertEquals( RDN, listener.result.getName() );
483         //assertEquals( listener.result.control.getChangeType(), ChangeType.MODIFY );
484     }*/
485 
486     
487     class JndiNotificationListener implements NamespaceChangeListener, ObjectChangeListener
488     {
489         boolean hasError = false;
490         ArrayList<EventObject> list = new ArrayList<EventObject>();
491         NamingExceptionEvent exceptionEvent = null;
492 
493         public void objectAdded( NamingEvent evt )
494         {
495             list.add( 0, evt );
496         }
497 
498 
499         public void objectRemoved( NamingEvent evt )
500         {
501             list.add( 0, evt );
502         }
503 
504 
505         public void objectRenamed( NamingEvent evt )
506         {
507             list.add( 0, evt );
508         }
509 
510 
511         public void namingExceptionThrown( NamingExceptionEvent evt )
512         {
513             hasError = true;
514             exceptionEvent = evt;
515             list.add( 0, evt );
516         }
517 
518 
519         public void objectChanged( NamingEvent evt )
520         {
521             list.add( 0, evt );
522         }
523     }
524 
525     
526     class PSearchListener implements Runnable
527     {
528         boolean isReady = false;
529         PSearchNotification result;
530         final PersistentSearchControl control;
531         LdapContext ctx;
532         NamingEnumeration<SearchResult> list;
533         
534         PSearchListener()
535         {
536             control = new PersistentSearchControl();
537         }
538 
539 
540         PSearchListener(PersistentSearchControl control)
541         {
542             this.control = control;
543         }
544 
545         
546         void close()
547         {
548             if ( list != null )
549             {
550                 try
551                 {
552                     list.close();
553                     LOG.debug( "PSearchListener: search naming enumeration closed()" );
554                 }
555                 catch ( Exception e )
556                 {
557                     LOG.error( "Error closing NamingEnumeration on PSearchListener", e );
558                 }
559             }
560             
561             if ( ctx != null )
562             {
563                 try
564                 {
565                     ctx.close();
566                     LOG.debug( "PSearchListener: search context closed()" );
567                 }
568                 catch ( Exception e )
569                 {
570                     LOG.error( "Error closing connection on PSearchListener", e );
571                 }
572             }
573         }
574 
575         
576         public void run()
577         {
578             LOG.debug( "PSearchListener.run() called." );
579             control.setCritical( true );
580             Control[] ctxCtls = new Control[]
581                 { control };
582 
583             try
584             {
585                 ctx = ( LdapContext ) getWiredContext( ldapService).lookup( BASE );
586                 ctx.setRequestControls( ctxCtls );
587                 isReady = true;
588                 LOG.debug( "PSearchListener is ready and about to issue persistent search request." );
589                 list = ctx.search( "", "objectClass=*", null );
590                 LOG.debug( "PSearchListener search request returned." );
591                 EntryChangeControlCodec ecControl = null;
592 
593                 while ( list.hasMore() )
594                 {
595                     LOG.debug( "PSearchListener search request got an item." );
596                     Control[] controls = null;
597                     SearchResult sresult = list.next();
598                     if ( sresult instanceof HasControls )
599                     {
600                         controls = ( ( HasControls ) sresult ).getControls();
601                         if ( controls != null )
602                         {
603                             for ( int ii = 0; ii < controls.length; ii++ )
604                             {
605                                 if ( controls[ii].getID().equals(
606                                     org.apache.directory.shared.ldap.message.EntryChangeControl.CONTROL_OID ) )
607                                 {
608                                     EntryChangeControlDecoder decoder = new EntryChangeControlDecoder();
609                                     ecControl = ( EntryChangeControlCodec ) decoder.decode( controls[ii].getEncodedValue() );
610                                 }
611                             }
612                         }
613                     }
614                     result = new PSearchNotification( sresult, ecControl );
615                     break;
616                 }
617                 LOG.debug( "PSearchListener broke out of while loop." );
618             }
619             catch ( Exception e )
620             {
621                 LOG.error( "PSearchListener encountered error", e );
622             }
623             finally
624             {
625             }
626         }
627     }
628 
629     
630     class PSearchNotification extends SearchResult
631     {
632         private static final long serialVersionUID = 1L;
633         final EntryChangeControlCodec control;
634 
635 
636         public PSearchNotification(SearchResult result, EntryChangeControlCodec control)
637         {
638             super( result.getName(), result.getClassName(), result.getObject(), result.getAttributes(), result
639                 .isRelative() );
640             this.control = control;
641         }
642 
643 
644         public String toString()
645         {
646             StringBuffer buf = new StringBuffer();
647             buf.append( "DN: " ).append( getName() ).append( "\n" );
648             if ( control != null )
649             {
650                 buf.append( "    EntryChangeControl =\n" );
651                 buf.append( "       changeType   : " ).append( control.getChangeType() ).append( "\n" );
652                 buf.append( "       previousDN   : " ).append( control.getPreviousDn() ).append( "\n" );
653                 buf.append( "       changeNumber : " ).append( control.getChangeNumber() ).append( "\n" );
654             }
655             return buf.toString();
656         }
657     }
658 }