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.operational;
021
022
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.List;
027import java.util.Set;
028import java.util.UUID;
029
030import org.apache.directory.api.ldap.model.constants.SchemaConstants;
031import org.apache.directory.api.ldap.model.entry.Attribute;
032import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
033import org.apache.directory.api.ldap.model.entry.DefaultModification;
034import org.apache.directory.api.ldap.model.entry.Entry;
035import org.apache.directory.api.ldap.model.entry.Modification;
036import org.apache.directory.api.ldap.model.entry.ModificationOperation;
037import org.apache.directory.api.ldap.model.entry.Value;
038import org.apache.directory.api.ldap.model.exception.LdapException;
039import org.apache.directory.api.ldap.model.exception.LdapNoPermissionException;
040import org.apache.directory.api.ldap.model.name.Ava;
041import org.apache.directory.api.ldap.model.name.Dn;
042import org.apache.directory.api.ldap.model.name.Rdn;
043import org.apache.directory.api.ldap.model.schema.AttributeType;
044import org.apache.directory.api.ldap.model.schema.AttributeTypeOptions;
045import org.apache.directory.api.ldap.model.schema.ObjectClass;
046import org.apache.directory.api.ldap.model.schema.SchemaManager;
047import org.apache.directory.api.util.DateUtils;
048import org.apache.directory.server.constants.ApacheSchemaConstants;
049import org.apache.directory.server.constants.ServerDNConstants;
050import org.apache.directory.server.core.api.DirectoryService;
051import org.apache.directory.server.core.api.InterceptorEnum;
052import org.apache.directory.server.core.api.entry.ClonedServerEntry;
053import org.apache.directory.server.core.api.filtering.EntryFilter;
054import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
055import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
056import org.apache.directory.server.core.api.interceptor.Interceptor;
057import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
058import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
059import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
060import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
061import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
062import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
063import org.apache.directory.server.core.api.interceptor.context.OperationContext;
064import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
065import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
066import org.apache.directory.server.core.api.partition.Partition;
067import org.apache.directory.server.core.api.partition.Subordinates;
068import org.apache.directory.server.core.shared.SchemaService;
069import org.apache.directory.server.i18n.I18n;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073
074/**
075 * An {@link Interceptor} that adds or modifies the default attributes
076 * of entries. There are six default attributes for now;
077 * <tt>'creatorsName'</tt>, <tt>'createTimestamp'</tt>, <tt>'modifiersName'</tt>,
078 * <tt>'modifyTimestamp'</tt>, <tt>entryUUID</tt> and <tt>entryCSN</tt>.
079 *
080 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
081 */
082public class OperationalAttributeInterceptor extends BaseInterceptor
083{
084    /** The LoggerFactory used by this Interceptor */
085    private static final Logger LOG = LoggerFactory.getLogger( OperationalAttributeInterceptor.class );
086
087    /** The denormalizer filter */
088    private final EntryFilter denormalizingSearchFilter = new OperationalAttributeDenormalizingSearchFilter();
089    
090    /** The filter that add the mandatory operational attributes */
091    private final EntryFilter operationalAttributeSearchFilter = new OperationalAttributeSearchFilter();
092    
093    /** The filter that add the subordinates operational attributes */
094    private final EntryFilter subordinatesSearchFilter = new SubordinatesSearchFilter();
095
096    /** The subschemasubentry Dn */
097    private Dn subschemaSubentryDn;
098
099    /** 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}