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.schema.registries.synchronizers;
021
022
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants;
028import org.apache.directory.api.ldap.model.constants.SchemaConstants;
029import org.apache.directory.api.ldap.model.entry.Attribute;
030import org.apache.directory.api.ldap.model.entry.Entry;
031import org.apache.directory.api.ldap.model.entry.Modification;
032import org.apache.directory.api.ldap.model.entry.ModificationOperation;
033import org.apache.directory.api.ldap.model.entry.Value;
034import org.apache.directory.api.ldap.model.exception.LdapException;
035import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
036import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
037import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
038import org.apache.directory.api.ldap.model.name.Dn;
039import org.apache.directory.api.ldap.model.name.Rdn;
040import org.apache.directory.api.ldap.model.schema.AttributeType;
041import org.apache.directory.api.ldap.model.schema.SchemaManager;
042import org.apache.directory.api.ldap.model.schema.registries.Schema;
043import org.apache.directory.api.ldap.schema.loader.SchemaEntityFactory;
044import org.apache.directory.api.util.Strings;
045import org.apache.directory.server.core.api.entry.ServerEntryUtils;
046import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
047import org.apache.directory.server.i18n.I18n;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051
052/**
053 * This class handle modifications made on a global schema. Modifications made
054 * on SchemaObjects are handled by the specific shcemaObject synchronizers.
055 *
056 * TODO poorly implemented - revisit the SchemaChangeHandler for this puppy
057 * and do it right.
058 *
059 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
060 */
061public class SchemaSynchronizer implements RegistrySynchronizer
062{
063    /** A logger for this class */
064    private static final Logger LOG = LoggerFactory.getLogger( SchemaSynchronizer.class );
065
066    private final SchemaEntityFactory factory;
067
068    private final SchemaManager schemaManager;
069
070    /** The m-disable AttributeType */
071    private final AttributeType disabledAT;
072
073    /** The CN attributeType */
074    private final AttributeType cnAT;
075
076    /** The m-dependencies AttributeType */
077    private final AttributeType dependenciesAT;
078
079    /** A static Dn referencing ou=schema */
080    private final Dn ouSchemaDn;
081
082
083    /**
084     * Creates and initializes a new instance of Schema synchronizer
085     *
086     * @param schemaManager The server schemaManager
087     * @throws Exception If something went wrong
088     */
089    public SchemaSynchronizer( SchemaManager schemaManager ) throws Exception
090    {
091        this.schemaManager = schemaManager;
092        disabledAT = schemaManager.lookupAttributeTypeRegistry( MetaSchemaConstants.M_DISABLED_AT );
093        factory = new SchemaEntityFactory();
094        cnAT = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.CN_AT );
095        dependenciesAT = schemaManager.lookupAttributeTypeRegistry( MetaSchemaConstants.M_DEPENDENCIES_AT );
096
097        ouSchemaDn = new Dn( schemaManager, SchemaConstants.OU_SCHEMA );
098    }
099
100
101    /**
102     * The only modification done on a schema element is on the m-disabled
103     * attributeType
104     *
105     * Depending in the existence of this attribute in the previous entry, we will
106     * have to update the entry or not.
107     */
108    @Override
109    public boolean modify( ModifyOperationContext modifyContext, Entry targetEntry, boolean cascade )
110        throws LdapException
111    {
112        Entry entry = modifyContext.getEntry();
113        List<Modification> mods = modifyContext.getModItems();
114        boolean hasModification = SCHEMA_UNCHANGED;
115
116        // Check if the entry has a m-disabled attribute
117        Attribute disabledInEntry = entry.get( disabledAT );
118        Modification disabledModification = ServerEntryUtils.getModificationItem( mods, disabledAT );
119
120        // The attribute might be present, but that does not mean we will change it.
121        // If it's absent, and if we have it in the previous entry, that mean we want
122        // to enable the schema
123        if ( disabledModification != null )
124        {
125            // We are trying to modify the m-disabled attribute.
126            ModificationOperation modification = disabledModification.getOperation();
127            Attribute attribute = disabledModification.getAttribute();
128
129            hasModification = modifyDisable( modifyContext, modification, attribute, disabledInEntry );
130        }
131        else if ( disabledInEntry != null )
132        {
133            hasModification = modifyDisable( modifyContext, ModificationOperation.REMOVE_ATTRIBUTE, null,
134                disabledInEntry );
135        }
136
137        return hasModification;
138    }
139
140
141    /**
142     * {@inheritDoc}
143     */
144    @Override
145    public void moveAndRename( Dn oriChildName, Dn newParentName, Rdn newRn, boolean deleteOldRn, Entry entry,
146        boolean cascaded ) throws LdapException
147    {
148        // Not implemented yet
149    }
150
151
152    /**
153     * Handles the addition of a metaSchema object to the schema partition.
154     *
155     * @param entry the attributes of the new metaSchema object
156     * @throws LdapException If the add failed
157     */
158    @Override
159    public void add( Entry entry ) throws LdapException
160    {
161        Dn dn = entry.getDn();
162        Dn parentDn = dn.getParent();
163
164        if ( !parentDn.equals( ouSchemaDn ) )
165        {
166            throw new LdapInvalidDnException( ResultCodeEnum.NAMING_VIOLATION, I18n.err( I18n.ERR_380,
167                ouSchemaDn.getName(),
168                parentDn.getName() ) );
169        }
170
171        // check if the new schema is enabled or disabled
172        boolean isEnabled = false;
173        Attribute disabled = entry.get( disabledAT );
174
175        if ( disabled == null )
176        {
177            // If the attribute is absent, then the schema is enabled by default
178            isEnabled = true;
179        }
180        else if ( !disabled.contains( "TRUE" ) )
181        {
182            isEnabled = true;
183        }
184
185        // check to see that all dependencies are resolved and loaded if this
186        // schema is enabled, otherwise check that the dependency schemas exist
187        checkForDependencies( isEnabled, entry );
188
189        /*
190         * There's a slight problem that may result when adding a metaSchema
191         * object if the addition of the physical entry fails.  If the schema
192         * is enabled when added in the condition tested below, that schema
193         * is added to the global registries.  We need to add this so subsequent
194         * schema entity additions are loaded into the registries as they are
195         * added to the schema partition.  However if the metaSchema object
196         * addition fails then we're left with this schema object looking like
197         * it is enabled in the registries object's schema hash.  The effects
198         * of this are unpredictable.
199         *
200         * This whole problem is due to the inability of these handlers to
201         * react to a failed operation.  To fix this we would need some way
202         * for these handlers to respond to failed operations and revert their
203         * effects on the registries.
204         *
205         * TODO: might want to add a set of failedOnXXX methods to the adapter
206         * where on failure the schema service calls the schema manager and it
207         * calls the appropriate methods on the respective handler.  This way
208         * the schema manager can rollback registry changes when LDAP operations
209         * fail.
210         */
211
212        if ( isEnabled )
213        {
214            Schema schema = factory.getSchema( entry );
215            schemaManager.load( schema );
216        }
217    }
218
219
220    /**
221     * Called to react to the deletion of a metaSchema object.  This method
222     * simply removes the schema from the loaded schema map of the global
223     * registries.
224     *
225     * @param entry the attributes of the metaSchema object
226     * @param cascade If we have to process recursively
227     * @throws LdapException If the delete failed
228     */
229    @Override
230    public void delete( Entry entry, boolean cascade ) throws LdapException
231    {
232        Attribute cn = entry.get( cnAT );
233        String schemaName = cn.getString();
234
235        // Before allowing a schema object to be deleted we must check
236        // to make sure it's not depended upon by another schema
237        Set<String> dependents = schemaManager.listDependentSchemaNames( schemaName );
238
239        if ( ( dependents != null ) && !dependents.isEmpty() )
240        {
241            String msg = I18n.err( I18n.ERR_381, dependents );
242            LOG.warn( msg );
243            throw new LdapUnwillingToPerformException(
244                ResultCodeEnum.UNWILLING_TO_PERFORM,
245                msg );
246        }
247
248        // no need to check if schema is enabled or disabled here
249        // if not in the loaded set there will be no negative effect
250        schemaManager.unload( schemaName );
251    }
252
253
254    /**
255     * Responds to the rdn (commonName) of the metaSchema object being
256     * changed.  Changes all the schema entities associated with the
257     * renamed schema so they now map to a new schema name.
258     *
259     * @param entry the entry of the metaSchema object before the rename
260     * @param newRdn the new commonName of the metaSchema object
261     * @param cascade If we have to process recursively
262     * @throws LdapException If the rename failed
263     */
264    @Override
265    public void rename( Entry entry, Rdn newRdn, boolean cascade ) throws LdapException
266    {
267        String rdnAttribute = newRdn.getNormType();
268        String rdnAttributeOid = schemaManager.getAttributeTypeRegistry().getOidByName( rdnAttribute );
269
270        if ( !rdnAttributeOid.equals( cnAT.getOid() ) )
271        {
272            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
273                I18n.err( I18n.ERR_382, rdnAttribute ) );
274        }
275
276        /*
277         * This operation has to do the following:
278         *
279         * [1] check and make sure there are no dependent schemas on the
280         *     one being renamed - if so an exception should result
281         *
282         * [2] make non-schema object registries modify the mapping
283         *     for their entities: non-schema object registries contain
284         *     objects that are not SchemaObjects and hence do not carry
285         *     their schema within the object as a property
286         *
287         * [3] make schema object registries do the same but the way
288         *     they do them will be different since these objects will
289         *     need to be replaced or will require a setter for the
290         *     schema name
291         */
292
293        // step [1]
294        /*
295        String schemaName = getSchemaName( entry.getDn() );
296        Set<String> dependents = schemaManager.listDependentSchemaNames( schemaName );
297        if ( ! dependents.isEmpty() )
298        {
299            throw new LdapUnwillingToPerformException(
300                "Cannot allow a rename on " + schemaName + " schema while it has depentents.",
301                ResultCodeEnum.UNWILLING_TO_PERFORM );
302        }
303
304        // check if the new schema is enabled or disabled
305        boolean isEnabled = false;
306        EntryAttribute disabled = entry.get( disabledAT );
307
308        if ( disabled == null )
309        {
310            isEnabled = true;
311        }
312        else if ( ! disabled.get().equals( "TRUE" ) )
313        {
314            isEnabled = true;
315        }
316
317        if ( ! isEnabled )
318        {
319            return;
320        }
321
322        // do steps 2 and 3 if the schema has been enabled and is loaded
323
324        // step [2]
325        String newSchemaName = ( String ) newRdn.getUpValue();
326        registries.getComparatorRegistry().renameSchema( schemaName, newSchemaName );
327        registries.getNormalizerRegistry().renameSchema( schemaName, newSchemaName );
328        registries.getSyntaxCheckerRegistry().renameSchema( schemaName, newSchemaName );
329
330        // step [3]
331        renameSchema( registries.getAttributeTypeRegistry(), schemaName, newSchemaName );
332        renameSchema( registries.getDitContentRuleRegistry(), schemaName, newSchemaName );
333        renameSchema( registries.getDitStructureRuleRegistry(), schemaName, newSchemaName );
334        renameSchema( registries.getMatchingRuleRegistry(), schemaName, newSchemaName );
335        renameSchema( registries.getMatchingRuleUseRegistry(), schemaName, newSchemaName );
336        renameSchema( registries.getNameFormRegistry(), schemaName, newSchemaName );
337        renameSchema( registries.getObjectClassRegistry(), schemaName, newSchemaName );
338        renameSchema( registries.getLdapSyntaxRegistry(), schemaName, newSchemaName );
339        */
340    }
341
342
343    /**
344     * Moves are not allowed for metaSchema objects so this always throws an
345     * UNWILLING_TO_PERFORM LdapException.
346     * 
347     * @param oriChildName The original child name
348     * @param newParentName The new parent name
349     * @param newRdn the new commonName of the metaSchema object
350     * @param deleteOldRdn If we need to delete the old Rdn 
351     * @param entry the entry of the metaSchema object before the rename
352     * @param cascade If we have to process recursively
353     * @throws LdapUnwillingToPerformException If the rename failed
354     */
355    public void moveAndRename( Dn oriChildName, Dn newParentName, String newRdn, boolean deleteOldRdn,
356        Entry entry, boolean cascade ) throws LdapUnwillingToPerformException
357    {
358        throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
359            I18n.err( I18n.ERR_383 ) );
360    }
361
362
363    /**
364     * Moves are not allowed for metaSchema objects so this always throws an
365     * UNWILLING_TO_PERFORM LdapException.
366     */
367    @Override
368    public void move( Dn oriChildName, Dn newParentName,
369        Entry entry, boolean cascade ) throws LdapUnwillingToPerformException
370    {
371        throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
372            I18n.err( I18n.ERR_383 ) );
373    }
374
375
376    // -----------------------------------------------------------------------
377    // private utility methods
378    // -----------------------------------------------------------------------
379
380    /**
381     * Modify the Disable flag (the flag can be set to true or false).
382     *
383     * We can ADD, REMOVE or MODIFY this flag. The following matrix expose what will be the consequences
384     * of this operation, depending on the current state
385     *
386     * <pre>
387     *                 +-------------------+--------------------+--------------------+
388     *     op/state    |       TRUE        |       FALSE        |       ABSENT       |
389     * +-------+-------+----------------------------------------+--------------------+
390     * | ADD   | TRUE  | do nothing        | do nothing         | disable the schema |
391     * |       +-------+-------------------+--------------------+--------------------+
392     * |       | FALSE | do nothing        | do nothing         | do nothing         |
393     * +-------+-------+-------------------+--------------------+--------------------+
394     * |REMOVE | N/A   | enable the schema | do nothing         | do nothing         |
395     * +-------+-------+-------------------+--------------------+--------------------+
396     * |MODIFY | TRUE  | do nothing        | disable the schema | disable the schema |
397     * |       +-------+-------------------+--------------------+--------------------+
398     * |       | FALSE | enable the schema | do nothing         |  do nothing        |
399     * +-------+-------+-------------------+--------------------+--------------------+
400     * </pre>
401     */
402    private boolean modifyDisable( ModifyOperationContext modifyContext, ModificationOperation modOp,
403        Attribute disabledInMods, Attribute disabledInEntry ) throws LdapException
404    {
405        Dn name = modifyContext.getDn();
406
407        switch ( modOp )
408        {
409        /*
410         * If the user is adding a new m-disabled attribute to an enabled schema,
411         * we check that the value is "TRUE" and disable that schema if so.
412         */
413            case ADD_ATTRIBUTE:
414                if ( disabledInEntry == null && "TRUE".equalsIgnoreCase( disabledInMods.getString() ) )
415                {
416                    return disableSchema( getSchemaName( name ) );
417                }
418
419                break;
420
421            /*
422             * If the user is removing the m-disabled attribute we check if the schema is currently
423             * disabled.  If so we enable the schema.
424             */
425            case REMOVE_ATTRIBUTE:
426                if ( ( disabledInEntry != null ) && ( "TRUE".equalsIgnoreCase( disabledInEntry.getString() ) ) )
427                {
428                    return enableSchema( getSchemaName( name ) );
429                }
430
431                break;
432
433            /*
434             * If the user is replacing the m-disabled attribute we check if the schema is
435             * currently disabled and enable it if the new state has it as enabled.  If the
436             * schema is not disabled we disable it if the mods set m-disabled to true.
437             */
438            case REPLACE_ATTRIBUTE:
439
440                boolean isCurrentlyDisabled = false;
441
442                if ( disabledInEntry != null )
443                {
444                    isCurrentlyDisabled = "TRUE".equalsIgnoreCase( disabledInEntry.getString() );
445                }
446
447                boolean isNewStateDisabled = false;
448
449                if ( disabledInMods != null )
450                {
451                    Value val = disabledInMods.get();
452
453                    if ( val == null )
454                    {
455                        isNewStateDisabled = false;
456                    }
457                    else
458                    {
459                        isNewStateDisabled = "TRUE".equalsIgnoreCase( val.getString() );
460                    }
461                }
462
463                if ( isCurrentlyDisabled && !isNewStateDisabled )
464                {
465                    return enableSchema( getSchemaName( name ) );
466                }
467
468                if ( !isCurrentlyDisabled && isNewStateDisabled )
469                {
470                    return disableSchema( getSchemaName( name ) );
471                }
472
473                break;
474
475            default:
476                throw new IllegalArgumentException( I18n.err( I18n.ERR_384, modOp ) );
477        }
478
479        return SCHEMA_UNCHANGED;
480    }
481
482
483    private String getSchemaName( Dn schema )
484    {
485        return schema.getRdn().getValue();
486    }
487
488
489    private boolean disableSchema( String schemaName ) throws LdapException
490    {
491        Schema schema = schemaManager.getLoadedSchema( schemaName );
492
493        if ( schema == null )
494        {
495            // This is not possible. We can't enable a schema which is not loaded.
496            String msg = I18n.err( I18n.ERR_85, schemaName );
497            LOG.error( msg );
498            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, msg );
499        }
500
501        return schemaManager.disable( schemaName );
502
503    }
504
505
506    /**
507     * Enabling a schema consist on switching all of its schema element to enable.
508     * We have to do it on a temporary registries.
509     */
510    private boolean enableSchema( String schemaName ) throws LdapException
511    {
512        Schema schema = schemaManager.getLoadedSchema( schemaName );
513
514        if ( schema == null )
515        {
516            // We have to load the schema before enabling it.
517            schemaManager.loadDisabled( schemaName );
518        }
519
520        return schemaManager.enable( schemaName );
521    }
522
523
524    /**
525     * Checks to make sure the dependencies either exist for disabled metaSchemas,
526     * or exist and are loaded (enabled) for enabled metaSchemas.
527     *
528     * @param isEnabled whether or not the new metaSchema is enabled
529     * @param entry the Attributes for the new metaSchema object
530     * @throws NamingException if the dependencies do not resolve or are not
531     * loaded (enabled)
532     */
533    private void checkForDependencies( boolean isEnabled, Entry entry ) throws LdapException
534    {
535        Attribute dependencies = entry.get( this.dependenciesAT );
536
537        if ( dependencies == null )
538        {
539            return;
540        }
541
542        if ( isEnabled )
543        {
544            // check to make sure all the dependencies are also enabled
545            Map<String, Schema> loaded = schemaManager.getRegistries().getLoadedSchemas();
546
547            for ( Value value : dependencies )
548            {
549                String dependency = value.getString();
550
551                if ( !loaded.containsKey( dependency ) )
552                {
553                    throw new LdapUnwillingToPerformException(
554                        ResultCodeEnum.UNWILLING_TO_PERFORM,
555                        "Unwilling to perform operation on enabled schema with disabled or missing dependencies: "
556                            + dependency );
557                }
558            }
559        }
560        else
561        {
562            for ( Value value : dependencies )
563            {
564                String dependency = value.getString();
565
566                if ( schemaManager.getLoadedSchema( Strings.toLowerCaseAscii( dependency ) ) == null )
567                {
568                    throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
569                        I18n.err( I18n.ERR_385, dependency ) );
570                }
571            }
572        }
573    }
574}