View Javadoc
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.core.operational;
21  
22  
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.Map;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.UUID;
29  
30  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
31  import org.apache.directory.api.ldap.model.entry.Attribute;
32  import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
33  import org.apache.directory.api.ldap.model.entry.DefaultModification;
34  import org.apache.directory.api.ldap.model.entry.Entry;
35  import org.apache.directory.api.ldap.model.entry.Modification;
36  import org.apache.directory.api.ldap.model.entry.ModificationOperation;
37  import org.apache.directory.api.ldap.model.entry.Value;
38  import org.apache.directory.api.ldap.model.exception.LdapException;
39  import org.apache.directory.api.ldap.model.exception.LdapNoPermissionException;
40  import org.apache.directory.api.ldap.model.name.Ava;
41  import org.apache.directory.api.ldap.model.name.Dn;
42  import org.apache.directory.api.ldap.model.name.Rdn;
43  import org.apache.directory.api.ldap.model.schema.AttributeType;
44  import org.apache.directory.api.ldap.model.schema.AttributeTypeOptions;
45  import org.apache.directory.api.ldap.model.schema.ObjectClass;
46  import org.apache.directory.api.ldap.model.schema.SchemaManager;
47  import org.apache.directory.api.util.DateUtils;
48  import org.apache.directory.server.constants.ApacheSchemaConstants;
49  import org.apache.directory.server.constants.ServerDNConstants;
50  import org.apache.directory.server.core.api.DirectoryService;
51  import org.apache.directory.server.core.api.InterceptorEnum;
52  import org.apache.directory.server.core.api.entry.ClonedServerEntry;
53  import org.apache.directory.server.core.api.filtering.EntryFilter;
54  import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
55  import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
56  import org.apache.directory.server.core.api.interceptor.Interceptor;
57  import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
58  import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
59  import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
60  import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
61  import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
62  import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
63  import org.apache.directory.server.core.api.interceptor.context.OperationContext;
64  import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
65  import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
66  import org.apache.directory.server.core.api.partition.Partition;
67  import org.apache.directory.server.core.api.partition.Subordinates;
68  import org.apache.directory.server.core.shared.SchemaService;
69  import org.apache.directory.server.i18n.I18n;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  
74  /**
75   * An {@link Interceptor} that adds or modifies the default attributes
76   * of entries. There are six default attributes for now;
77   * <tt>'creatorsName'</tt>, <tt>'createTimestamp'</tt>, <tt>'modifiersName'</tt>,
78   * <tt>'modifyTimestamp'</tt>, <tt>entryUUID</tt> and <tt>entryCSN</tt>.
79   *
80   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
81   */
82  public class OperationalAttributeInterceptor extends BaseInterceptor
83  {
84      /** The LoggerFactory used by this Interceptor */
85      private static final Logger LOG = LoggerFactory.getLogger( OperationalAttributeInterceptor.class );
86  
87      /** The denormalizer filter */
88      private final EntryFilter denormalizingSearchFilter = new OperationalAttributeDenormalizingSearchFilter();
89      
90      /** The filter that add the mandatory operational attributes */
91      private final EntryFilter operationalAttributeSearchFilter = new OperationalAttributeSearchFilter();
92      
93      /** The filter that add the subordinates operational attributes */
94      private final EntryFilter subordinatesSearchFilter = new SubordinatesSearchFilter();
95  
96      /** The subschemasubentry Dn */
97      private Dn subschemaSubentryDn;
98  
99      /** The admin Dn */
100     private Dn adminDn;
101 
102     /**
103      * the search result filter to use for collective attribute injection
104      */
105     private class OperationalAttributeDenormalizingSearchFilter implements EntryFilter
106     {
107         /**
108          * {@inheritDoc}
109          */
110         @Override
111         public boolean accept( SearchOperationContext operation, Entry entry ) throws LdapException
112         {
113             if ( operation.getReturningAttributesString() == null )
114             {
115                 return true;
116             }
117 
118             // Denormalize the operational Attributes
119             denormalizeEntryOpAttrs( entry );
120             
121             return true;
122         }
123 
124 
125         /**
126          * {@inheritDoc}
127          */
128         @Override
129         public String toString( String tabs )
130         {
131             return tabs + "OperationalAttributeDenormalizingSearchFilter";
132         }
133     }
134 
135     
136     /**
137      * the search result filter to use for the addition of mandatory operational attributes
138      */
139     private class OperationalAttributeSearchFilter implements EntryFilter
140     {
141         /**
142          * {@inheritDoc}
143          */
144         @Override
145         public boolean accept( SearchOperationContext operation, Entry entry ) throws LdapException
146         {
147             if ( operation.getReturningAttributesString() == null )
148             {
149                 return true;
150             }
151 
152             // Add the SubschemaSubentry AttributeType if it's requested
153             SchemaManager schemaManager = operation.getSession().getDirectoryService().getSchemaManager();
154             
155             if ( operation.isAllOperationalAttributes()
156                 || operation.getReturningAttributes().contains( 
157                     new AttributeTypeOptions( schemaManager.getAttributeType( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ) ) ) )
158             {
159                 AttributeType subschemaSubentryAt = schemaManager.getAttributeType( SchemaConstants.SUBSCHEMA_SUBENTRY_AT );
160                 entry.add( new DefaultAttribute( subschemaSubentryAt, 
161                     directoryService.getPartitionNexus().getRootDseValue( subschemaSubentryAt ) ) );
162             }
163 
164             return true;
165         }
166 
167 
168         /**
169          * {@inheritDoc}
170          */
171         @Override
172         public String toString( String tabs )
173         {
174             return tabs + "OperationalAttributeSearchFilter";
175         }
176     }
177 
178     
179     /**
180      * The search result filter to use for the addition of the subordinates attributes, if requested
181      */
182     private class SubordinatesSearchFilter implements EntryFilter
183     {
184         /**
185          * {@inheritDoc}
186          */
187         @Override
188         public boolean accept( SearchOperationContext searchOperationContext, Entry entry ) throws LdapException
189         {
190             // Add the nbChildren/nbSubordinates attributes if required
191             processSubordinates( searchOperationContext, searchOperationContext.getReturningAttributes(), 
192                 searchOperationContext.isAllOperationalAttributes(), entry );
193 
194             return true;
195         }
196 
197 
198         /**
199          * {@inheritDoc}
200          */
201         @Override
202         public String toString( String tabs )
203         {
204             return tabs + "SubordinatesSearchFilter";
205         }
206     }
207 
208     
209     /**
210      * Creates the operational attribute management service interceptor.
211      */
212     public OperationalAttributeInterceptor()
213     {
214         super( InterceptorEnum.OPERATIONAL_ATTRIBUTE_INTERCEPTOR );
215     }
216 
217 
218     @Override
219     public void init( DirectoryService directoryService ) throws LdapException
220     {
221         super.init( directoryService );
222 
223         // stuff for dealing with subentries (garbage for now)
224         Value subschemaSubentry = directoryService.getPartitionNexus().getRootDseValue(
225             directoryService.getAtProvider().getSubschemaSubentry() );
226         subschemaSubentryDn = dnFactory.create( subschemaSubentry.getString() );
227 
228         // Create the Admin Dn
229         adminDn = dnFactory.create( ServerDNConstants.ADMIN_SYSTEM_DN );
230     }
231 
232 
233     @Override
234     public void destroy()
235     {
236     }
237 
238 
239     /**
240      * Check if we have to add an operational attribute, or if the admin has injected one
241      */
242     private boolean checkAddOperationalAttribute( boolean isAdmin, Entry entry, AttributeType attribute )
243         throws LdapException
244     {
245         if ( entry.containsAttribute( attribute ) )
246         {
247             if ( !isAdmin )
248             {
249                 // Wrong !
250                 String message = I18n.err( I18n.ERR_30, attribute );
251                 LOG.error( message );
252                 throw new LdapNoPermissionException( message );
253             }
254             else
255             {
256                 return true;
257             }
258         }
259         else
260         {
261             return false;
262         }
263     }
264 
265 
266     /**
267      * Adds extra operational attributes to the entry before it is added.
268      * 
269      * We add those attributes :
270      * - creatorsName
271      * - createTimestamp
272      * - entryCSN
273      * - entryUUID
274      */
275     /**
276      * {@inheritDoc}
277      */
278     @Override
279     public void add( AddOperationContext addContext ) throws LdapException
280     {
281         String principal = getPrincipal( addContext ).getName();
282 
283         Entry entry = addContext.getEntry();
284 
285         // If we are using replication, the below four OAs may already be present and we retain
286         // those values if the user is admin.
287         boolean isAdmin = addContext.getSession().getAuthenticatedPrincipal().getDn().equals( adminDn );
288 
289         // The EntryUUID attribute
290         if ( !checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getEntryUUID() ) )
291         {
292             entry.put( directoryService.getAtProvider().getEntryUUID(), UUID.randomUUID().toString() );
293         }
294 
295         // The EntryCSN attribute
296         if ( !checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getEntryCSN() ) )
297         {
298             entry.put( directoryService.getAtProvider().getEntryCSN(), directoryService.getCSN().toString() );
299         }
300 
301         // The CreatorsName attribute
302         if ( !checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getCreatorsName() ) )
303         {
304             entry.put( directoryService.getAtProvider().getCreatorsName(), principal );
305         }
306 
307         // The CreateTimeStamp attribute
308         if ( !checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getCreateTimestamp() ) )
309         {
310             entry.put( directoryService.getAtProvider().getCreateTimestamp(), DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );        
311         }
312 
313         // Now, check that the user does not add operational attributes
314         // The accessControlSubentries attribute
315         checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getAccessControlSubentries() );
316 
317         // The CollectiveAttributeSubentries attribute
318         checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider()
319             .getCollectiveAttributeSubentries() );
320 
321         // The TriggerExecutionSubentries attribute
322         checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getTriggerExecutionSubentries() );
323 
324         // The SubSchemaSybentry attribute
325         checkAddOperationalAttribute( isAdmin, entry, directoryService.getAtProvider().getSubschemaSubentry() );
326 
327         next( addContext );
328     }
329 
330 
331     /**
332      * {@inheritDoc}
333      */
334     @Override
335     public Entry lookup( LookupOperationContext lookupContext ) throws LdapException
336     {
337         Dn dn = lookupContext.getDn();
338 
339         if ( dn.getNormName().equals( subschemaSubentryDn.getNormName() ) )
340         {
341             Entry serverEntry = SchemaService.getSubschemaEntry( directoryService, lookupContext );
342             serverEntry.setDn( dn );
343 
344             return serverEntry;
345         }
346 
347         Entry entry = next( lookupContext );
348 
349         denormalizeEntryOpAttrs( entry );
350         
351         // Add the nbChildren/nbSubordinates attributes if required
352         processSubordinates( lookupContext, lookupContext.getReturningAttributes(), lookupContext.isAllOperationalAttributes(), entry );
353 
354         return entry;
355     }
356 
357 
358     /**
359      * {@inheritDoc}
360      */
361     @Override
362     public void modify( ModifyOperationContext modifyContext ) throws LdapException
363     {
364         // We must check that the user hasn't injected either the modifiersName
365         // or the modifyTimestamp operational attributes : they are not supposed to be
366         // added at this point EXCEPT in cases of replication by a admin user.
367         // If so, remove them, and if there are no more attributes, simply return.
368         // otherwise, inject those values into the list of modifications
369         List<Modification> mods = modifyContext.getModItems();
370 
371         boolean isAdmin = modifyContext.getSession().getAuthenticatedPrincipal().getDn().equals( adminDn );
372 
373         boolean modifierAtPresent = false;
374         boolean modifiedTimeAtPresent = false;
375         boolean entryCsnAtPresent = false;
376         Dn dn = modifyContext.getDn();
377 
378         for ( Modification modification : mods )
379         {
380             AttributeType attributeType = modification.getAttribute().getAttributeType();
381 
382             if ( attributeType.equals( directoryService.getAtProvider().getModifiersName() ) )
383             {
384                 if ( !isAdmin )
385                 {
386                     String message = I18n.err( I18n.ERR_31 );
387                     LOG.error( message );
388                     throw new LdapNoPermissionException( message );
389                 }
390                 else
391                 {
392                     modifierAtPresent = true;
393                 }
394             }
395 
396             if ( attributeType.equals( directoryService.getAtProvider().getModifyTimestamp() ) )
397             {
398                 if ( !isAdmin )
399                 {
400                     String message = I18n.err( I18n.ERR_30, attributeType );
401                     LOG.error( message );
402                     throw new LdapNoPermissionException( message );
403                 }
404                 else
405                 {
406                     modifiedTimeAtPresent = true;
407                 }
408             }
409 
410             if ( attributeType.equals( directoryService.getAtProvider().getEntryCSN() ) )
411             {
412                 if ( !isAdmin )
413                 {
414                     String message = I18n.err( I18n.ERR_30, attributeType );
415                     LOG.error( message );
416                     throw new LdapNoPermissionException( message );
417                 }
418                 else
419                 {
420                     entryCsnAtPresent = true;
421                 }
422             }
423 
424             if ( PWD_POLICY_STATE_ATTRIBUTE_TYPES.contains( attributeType ) && !isAdmin )
425             {
426                 String message = I18n.err( I18n.ERR_30, attributeType );
427                 LOG.error( message );
428                 throw new LdapNoPermissionException( message );
429             }
430         }
431 
432         // Add the modification AT only if we are not trying to modify the SubentrySubschema
433         if ( !dn.equals( subschemaSubentryDn ) )
434         {
435             if ( !modifierAtPresent )
436             {
437                 // Inject the ModifiersName AT if it's not present
438                 Attribute attribute = new DefaultAttribute( directoryService.getAtProvider().getModifiersName(),
439                     getPrincipal( modifyContext ).getName() );
440 
441                 Modification modifiersName = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
442                     attribute );
443 
444                 mods.add( modifiersName );
445             }
446 
447             if ( !modifiedTimeAtPresent )
448             {
449                 // Inject the ModifyTimestamp AT if it's not present
450                 Attribute attribute = new DefaultAttribute( directoryService.getAtProvider().getModifyTimestamp(),
451                     DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
452 
453                 Modification timestamp = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
454 
455                 mods.add( timestamp );
456             }
457 
458             if ( !entryCsnAtPresent )
459             {
460                 String csn = directoryService.getCSN().toString();
461                 Attribute attribute = new DefaultAttribute( directoryService.getAtProvider().getEntryCSN(), csn );
462                 Modification updatedCsn = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute );
463                 mods.add( updatedCsn );
464             }
465         }
466 
467         // Go down in the chain
468         next( modifyContext );
469     }
470 
471 
472     /**
473      * {@inheritDoc}
474      */
475     @Override
476     public void move( MoveOperationContext moveContext ) throws LdapException
477     {
478         Entry modifiedEntry = moveContext.getOriginalEntry().clone();
479         modifiedEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal( moveContext ).getName() );
480         modifiedEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
481 
482         Attribute csnAt = new DefaultAttribute( directoryService.getAtProvider().getEntryCSN(), directoryService
483             .getCSN().toString() );
484         modifiedEntry.put( csnAt );
485 
486         modifiedEntry.setDn( moveContext.getNewDn() );
487         moveContext.setModifiedEntry( modifiedEntry );
488 
489         next( moveContext );
490     }
491 
492 
493     /**
494      * {@inheritDoc}
495      */
496     @Override
497     public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
498     {
499         Entry modifiedEntry = moveAndRenameContext.getModifiedEntry();
500         modifiedEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal( moveAndRenameContext ).getName() );
501         modifiedEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
502         modifiedEntry.setDn( moveAndRenameContext.getNewDn() );
503 
504         Attribute csnAt = new DefaultAttribute( directoryService.getAtProvider().getEntryCSN(), directoryService
505             .getCSN().toString() );
506         modifiedEntry.put( csnAt );
507 
508         moveAndRenameContext.setModifiedEntry( modifiedEntry );
509 
510         next( moveAndRenameContext );
511     }
512 
513 
514     /**
515      * {@inheritDoc}
516      */
517     @Override
518     public void rename( RenameOperationContext renameContext ) throws LdapException
519     {
520         Entry entry = ( ( ClonedServerEntry ) renameContext.getEntry() ).getClonedEntry();
521         entry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal( renameContext ).getName() );
522         entry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
523 
524         Entry modifiedEntry = renameContext.getOriginalEntry().clone();
525         modifiedEntry.put( SchemaConstants.MODIFIERS_NAME_AT, getPrincipal( renameContext ).getName() );
526         modifiedEntry.put( SchemaConstants.MODIFY_TIMESTAMP_AT, DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
527 
528         Attribute csnAt = new DefaultAttribute( directoryService.getAtProvider().getEntryCSN(), directoryService
529             .getCSN().toString() );
530         modifiedEntry.put( csnAt );
531 
532         renameContext.setModifiedEntry( modifiedEntry );
533 
534         next( renameContext );
535     }
536 
537 
538     /**
539      * {@inheritDoc}
540      */
541     @Override
542     public EntryFilteringCursor search( SearchOperationContext searchContext ) throws LdapException
543     {
544         EntryFilteringCursor cursor = next( searchContext );
545 
546         if ( searchContext.isAllOperationalAttributes()
547             || ( ( searchContext.getReturningAttributes() != null ) && !searchContext.getReturningAttributes().isEmpty() ) )
548         {
549             if ( directoryService.isDenormalizeOpAttrsEnabled() )
550             {
551                 cursor.addEntryFilter( denormalizingSearchFilter );
552             }
553 
554             cursor.addEntryFilter( operationalAttributeSearchFilter );
555             cursor.addEntryFilter( subordinatesSearchFilter );
556             
557             return cursor;
558         }
559 
560         return cursor;
561     }
562 
563 
564     @Override
565     public void delete( DeleteOperationContext deleteContext ) throws LdapException
566     {
567         // insert a new CSN into the entry, this is for replication
568         Entry entry = deleteContext.getEntry();
569         Attribute csnAt = new DefaultAttribute( directoryService.getAtProvider().getEntryCSN(), directoryService
570             .getCSN().toString() );
571         entry.put( csnAt );
572 
573         next( deleteContext );
574     }
575 
576 
577     private void denormalizeEntryOpAttrs( Entry entry ) throws LdapException
578     {
579         if ( directoryService.isDenormalizeOpAttrsEnabled() )
580         {
581             Attribute attr = entry.get( SchemaConstants.CREATORS_NAME_AT );
582 
583             if ( attr != null )
584             {
585                 Dn creatorsName = dnFactory.create( attr.getString() );
586 
587                 attr.clear();
588                 attr.add( denormalizeTypes( creatorsName ).getName() );
589             }
590 
591             attr = entry.get( SchemaConstants.MODIFIERS_NAME_AT );
592 
593             if ( attr != null )
594             {
595                 Dn modifiersName = dnFactory.create( attr.getString() );
596 
597                 attr.clear();
598                 attr.add( denormalizeTypes( modifiersName ).getName() );
599             }
600 
601             attr = entry.get( ApacheSchemaConstants.SCHEMA_MODIFIERS_NAME_AT );
602 
603             if ( attr != null )
604             {
605                 Dn modifiersName = dnFactory.create( attr.getString() );
606 
607                 attr.clear();
608                 attr.add( denormalizeTypes( modifiersName ).getName() );
609             }
610         }
611     }
612 
613 
614     /**
615      * Does not create a new Dn but alters existing Dn by using the first
616      * short name for an attributeType definition.
617      * 
618      * @param dn the normalized distinguished name
619      * @return the distinguished name denormalized
620      * @throws Exception if there are problems denormalizing
621      */
622     private Dn denormalizeTypes( Dn dn ) throws LdapException
623     {
624         Dn newDn = new Dn( schemaManager );
625         int size = dn.size();
626 
627         for ( int pos = 0; pos < size; pos++ )
628         {
629             Rdn rdn = dn.getRdn( size - 1 - pos );
630 
631             if ( rdn.size() == 0 )
632             {
633                 newDn = newDn.add( new Rdn() );
634                 continue;
635             }
636             else if ( rdn.size() == 1 )
637             {
638                 String name = schemaManager.lookupAttributeTypeRegistry( rdn.getNormType() ).getName();
639                 String value = rdn.getValue();
640                 newDn = newDn.add( new Rdn( name, value ) );
641                 continue;
642             }
643 
644             // below we only process multi-valued rdns
645             StringBuilder buf = new StringBuilder();
646 
647             for ( Iterator<Ava> atavs = rdn.iterator(); atavs.hasNext(); /**/)
648             {
649                 Ava atav = atavs.next();
650                 String type = schemaManager.lookupAttributeTypeRegistry( rdn.getNormType() ).getName();
651                 buf.append( type ).append( '=' ).append( atav.getValue().getString() );
652 
653                 if ( atavs.hasNext() )
654                 {
655                     buf.append( '+' );
656                 }
657             }
658 
659             newDn = newDn.add( new Rdn( buf.toString() ) );
660         }
661 
662         return newDn;
663     }
664     
665     
666     private void processSubordinates( OperationContext operationContext, Set<AttributeTypeOptions> returningAttributes, 
667         boolean allAttributes, Entry entry ) throws LdapException
668     {
669         // Bypass the rootDSE : we won't get the nbChildren and nbSubordiantes for this special entry
670         if ( Dn.isNullOrEmpty( entry.getDn() ) )
671         {
672             return;
673         }
674 
675         // Add the Subordinates AttributeType if it's requested
676         AttributeType nbChildrenAt = directoryService.getAtProvider().getNbChildren();
677         AttributeTypeOptions nbChildrenAto = new AttributeTypeOptions( nbChildrenAt );
678         AttributeType nbSubordinatesAt = directoryService.getAtProvider().getNbSubordinates();
679         AttributeTypeOptions nbSubordinatesAto = new AttributeTypeOptions( nbSubordinatesAt );
680         AttributeType hasSubordinatesAt = directoryService.getAtProvider().getHasSubordinates();
681         AttributeTypeOptions hasSubordinatesAto = new AttributeTypeOptions( hasSubordinatesAt );
682         AttributeType structuralObjectClassAt = directoryService.getAtProvider().getStructuralObjectClass();
683         AttributeTypeOptions structuralObjectClassAto = new AttributeTypeOptions( structuralObjectClassAt );
684         
685         if ( returningAttributes != null )
686         {
687             boolean nbChildrenRequested = returningAttributes.contains( nbChildrenAto ) || allAttributes;
688             boolean nbSubordinatesRequested = returningAttributes.contains( nbSubordinatesAto ) || allAttributes;
689             boolean hasSubordinatesRequested = returningAttributes.contains( hasSubordinatesAto ) || allAttributes;
690             boolean structuralObjectClassRequested = returningAttributes.contains( structuralObjectClassAto ) || allAttributes;
691 
692             if ( nbChildrenRequested || nbSubordinatesRequested || hasSubordinatesRequested 
693                 || structuralObjectClassRequested )
694             {
695                 Partition partition = directoryService.getPartitionNexus().getPartition( entry.getDn() );
696                 Subordinates subordinates = partition.getSubordinates( operationContext.getTransaction(), entry );
697                 
698                 long nbChildren = subordinates.getNbChildren();
699                 long nbSubordinates = subordinates.getNbSubordinates();
700                 
701                 // Inject the nbChildren OpAttr if needed
702                 if ( nbChildrenRequested )
703                 {
704                     entry.add( new DefaultAttribute( nbChildrenAt, 
705                         Long.toString( nbChildren ) ) );
706                 }
707     
708                 // Inject the nbSubordinates OpAttr if needed
709                 if ( nbSubordinatesRequested )
710                 { 
711                     entry.add( new DefaultAttribute( nbSubordinatesAt,
712                         Long.toString( nbSubordinates ) ) );
713                 }
714                 
715                 // Inject the hasSubordinates OpAttr if needed
716                 if ( hasSubordinatesRequested )
717                 { 
718                     if ( nbSubordinates > 0 )
719                     {
720                         entry.add( new DefaultAttribute( hasSubordinatesAt, "TRUE" ) );
721                     }
722                     else
723                     {
724                         entry.add( new DefaultAttribute( hasSubordinatesAt, "FALSE" ) );
725                     }
726                 }
727 
728                 // Inject the structuralObjectclass OpAttr if needed
729                 if ( structuralObjectClassRequested )
730                 {
731                     Attribute objectClasses = entry.get( SchemaConstants.OBJECT_CLASS_AT );
732                     Map<String, ObjectClass> superiors = new HashMap<>();
733                     ObjectClass[] objectClassArray = new ObjectClass[objectClasses.size()];
734                     int nbStructural = 0;
735                     
736                     // First get all the structural objectClasses
737                     for ( Value objectClassValue : objectClasses )
738                     {
739                         ObjectClass objectClass = 
740                             schemaManager.getObjectClassRegistry().get( objectClassValue.getNormalized() );
741                         
742                         if ( objectClass.isStructural() )
743                         {
744                             objectClassArray[nbStructural++] = objectClass;
745                             
746                             // We can only have one superior objectClass for Structural ObjectClass
747                             superiors.put( objectClass.getSuperiors().get( 0 ).getOid(), objectClass );
748                         }
749                     }
750 
751                     // Then find the top of them
752                     if ( nbStructural == 1 )
753                     {
754                         entry.add( new DefaultAttribute( structuralObjectClassAt, 
755                             objectClassArray[0].getName() ) );
756                     }
757                     else
758                     {
759                         ObjectClass topStructural = objectClassArray[0];
760 
761                         for ( ObjectClass oc : objectClassArray )
762                         {
763                             if ( !superiors.containsKey( oc.getOid() ) )
764                             {
765                                 // We are done : the current OC is not the superior of any other
766                                 // OC, this is necessarily the top level one
767                                 entry.add( new DefaultAttribute( structuralObjectClassAt, oc.getName() ) );
768                                 break;
769                             }
770                         }
771                     }
772                 }
773             }
774         }
775     }
776 }