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.api.schema;
21  
22  
23  import java.util.ArrayList;
24  import java.util.List;
25  
26  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
27  import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
28  import org.apache.directory.api.ldap.model.entry.DefaultModification;
29  import org.apache.directory.api.ldap.model.entry.Entry;
30  import org.apache.directory.api.ldap.model.entry.Modification;
31  import org.apache.directory.api.ldap.model.entry.ModificationOperation;
32  import org.apache.directory.api.ldap.model.exception.LdapContextNotEmptyException;
33  import org.apache.directory.api.ldap.model.exception.LdapException;
34  import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
35  import org.apache.directory.api.ldap.model.filter.ExprNode;
36  import org.apache.directory.api.ldap.model.filter.PresenceNode;
37  import org.apache.directory.api.ldap.model.message.SearchRequest;
38  import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
39  import org.apache.directory.api.ldap.model.message.SearchScope;
40  import org.apache.directory.api.ldap.model.message.controls.Cascade;
41  import org.apache.directory.api.ldap.model.name.Dn;
42  import org.apache.directory.api.ldap.model.schema.AttributeType;
43  import org.apache.directory.api.ldap.model.schema.SchemaManager;
44  import org.apache.directory.api.ldap.model.schema.SchemaUtils;
45  import org.apache.directory.api.util.DateUtils;
46  import org.apache.directory.server.constants.ApacheSchemaConstants;
47  import org.apache.directory.server.core.api.CoreSession;
48  import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
49  import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
50  import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
51  import org.apache.directory.server.core.api.interceptor.context.HasEntryOperationContext;
52  import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
53  import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
54  import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
55  import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
56  import org.apache.directory.server.core.api.interceptor.context.OperationContext;
57  import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
58  import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
59  import org.apache.directory.server.core.api.interceptor.context.UnbindOperationContext;
60  import org.apache.directory.server.core.api.partition.AbstractPartition;
61  import org.apache.directory.server.core.api.partition.Partition;
62  import org.apache.directory.server.core.api.partition.PartitionReadTxn;
63  import org.apache.directory.server.core.api.partition.PartitionTxn;
64  import org.apache.directory.server.core.api.partition.PartitionWriteTxn;
65  import org.apache.directory.server.core.api.partition.Subordinates;
66  import org.apache.directory.server.core.api.schema.registries.synchronizers.RegistrySynchronizerAdaptor;
67  import org.apache.directory.server.i18n.I18n;
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  
72  /**
73   * A special partition designed to contain the portion of the DIT where schema
74   * information for the server is stored.
75   * 
76   * In an effort to make sure that all Partition implementations are equal
77   * citizens to ApacheDS we want to be able to swap in and out any kind of
78   * Partition to store schema.  This also has the added advantage of making
79   * sure the core, and hence the server is not dependent on any specific
80   * partition, which reduces coupling in the server's modules.
81   * 
82   * The SchemaPartition achieves this by not really being a backing store
83   * itself for the schema entries.  It instead delegates to another Partition
84   * via containment.  It delegates all calls to this contained Partition. While
85   * doing so it also manages certain things:
86   * 
87   * <ol>
88   *   <li>Checks that schema changes are valid.</li>
89   *   <li>Updates the schema Registries on valid schema changes making sure
90   *       the schema on disk is in sync with the schema in memory.
91   *   </li>
92   *   <li>Will eventually manage transaction based changes to schema where
93   *       between some sequence of operations the schema may be inconsistent.
94   *   </li>
95   *   <li>Delegates read/write operations to contained Partition.</li>
96   *   <li>
97   *       Responsible for initializing schema for the entire server.  ApacheDS
98   *       cannot start up other partitions until this Partition is started
99   *       without having access to the Registries.  This Partition supplies the
100  *       Registries on initialization for the server.  That's one of it's core
101  *       responsibilities.
102  *   </li>
103  * </ol>
104  * 
105  * So by containing another Partition, we abstract the storage mechanism away
106  * from the management responsibilities while decoupling the server from a
107  * specific partition implementation and removing complexity in the Schema
108  * interceptor service which before managed synchronization.
109  *
110  * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
111  */
112 public final class SchemaPartition extends AbstractPartition
113 {
114     /** the logger */
115     private static final Logger LOG = LoggerFactory.getLogger( SchemaPartition.class );
116 
117     /** the fixed id: 'schema' */
118     private static final String SCHEMA_ID = "schema";
119 
120     /** the wrapped Partition */
121     private Partition wrapped;
122 
123     /** registry synchronizer adaptor */
124     private RegistrySynchronizerAdaptor synchronizer;
125 
126     /** A static Dn for the ou=schemaModifications entry */
127     private Dn schemaModificationDN;
128 
129     /** A static Dn for the ou=schema partition */
130     private Dn schemaDN;
131 
132     /** The ObjectClass AttributeType */
133     private AttributeType objectClassAT;
134 
135 
136     public SchemaPartition( SchemaManager schemaManager )
137     {
138         try
139         {
140             schemaDN = new Dn( schemaManager, SchemaConstants.OU_SCHEMA );
141         }
142         catch ( LdapInvalidDnException lide )
143         {
144             // Nothing to do : this is a valid DN anyways
145         }
146 
147         id = SCHEMA_ID;
148         suffixDn = schemaDN;
149         this.schemaManager = schemaManager;
150         objectClassAT = schemaManager.getAttributeType( SchemaConstants.OBJECT_CLASS_AT_OID );
151     }
152 
153 
154     /**
155      * Sets the wrapped {@link Partition} which must be supplied or
156      * {@link Partition#initialize()} will fail with a NullPointerException.
157      *
158      * @param wrapped the Partition being wrapped
159      */
160     public void setWrappedPartition( Partition wrapped )
161     {
162         if ( this.isInitialized() )
163         {
164             throw new IllegalStateException( I18n.err( I18n.ERR_429 ) );
165         }
166 
167         this.wrapped = wrapped;
168     }
169 
170 
171     /**
172      * Gets the {@link Partition} being wrapped.
173      *
174      * @return the wrapped Partition
175      */
176     public Partition getWrappedPartition()
177     {
178         return wrapped;
179     }
180 
181 
182     /**
183      * Has no affect: the id is fixed at {@link SchemaPartition#SCHEMA_ID}: 'schema'.
184      * A warning is logged.
185      */
186     @Override
187     public void setId( String id )
188     {
189         LOG.warn( "This partition's ID is fixed: {}", SCHEMA_ID );
190     }
191 
192 
193     // -----------------------------------------------------------------------
194     // Partition Interface Method Overrides
195     // -----------------------------------------------------------------------
196 
197     /**
198      * {@inheritDoc}
199      */
200     @Override
201     public void sync() throws LdapException
202     {
203         wrapped.sync();
204     }
205 
206 
207     /**
208      * {@inheritDoc}
209      */
210     @Override
211     protected void doRepair() throws LdapException
212     {
213         // Nothing to do
214     }
215     
216     
217     /**
218      * {@inheritDoc}
219      */
220     @Override
221     protected void doInit() throws LdapException
222     {
223         if ( !initialized )
224         {
225             // -----------------------------------------------------------------------
226             // Load apachemeta schema from within the ldap-schema Jar with all the
227             // schema it depends on.  This is a minimal mandatory set of schemas.
228             // -----------------------------------------------------------------------
229             wrapped.setId( SCHEMA_ID );
230             wrapped.setSuffixDn( schemaDN );
231             wrapped.setSchemaManager( schemaManager );
232 
233             try
234             {
235                 // The schemaManager *must* be relaxed, otherwise disabled schema
236                 // won't be loaded properly
237                 schemaManager.setRelaxed();
238                 
239                 // Load the schemas
240                 wrapped.initialize();
241                 
242                 // Now we can get the schemaManager back to strict mode
243                 schemaManager.setStrict();
244 
245                 synchronizer = new RegistrySynchronizerAdaptor( schemaManager );
246             }
247             catch ( Exception e )
248             {
249                 LOG.error( I18n.err( I18n.ERR_90 ), e );
250                 throw new RuntimeException( e );
251             }
252 
253             schemaModificationDN = new Dn( schemaManager, SchemaConstants.SCHEMA_MODIFICATIONS_DN );
254         }
255     }
256 
257 
258     /**
259      * {@inheritDoc}
260      */
261     @Override
262     protected void doDestroy( PartitionTxn partitionTxn )
263     {
264         try
265         {
266             wrapped.destroy( partitionTxn );
267         }
268         catch ( Exception e )
269         {
270             LOG.error( I18n.err( I18n.ERR_91 ), e );
271             throw new RuntimeException( e );
272         }
273 
274         initialized = false;
275     }
276 
277 
278     // -----------------------------------------------------------------------
279     // Partition Interface Methods
280     // -----------------------------------------------------------------------
281 
282     /**
283      * {@inheritDoc}
284      */
285     public void add( AddOperationContext addContext ) throws LdapException
286     {
287         // At this point, the added SchemaObject does not exist in the partition
288         // We have to check if it's enabled and then inject it into the registries
289         // but only if it does not break the server.
290         synchronizer.add( addContext );
291 
292         // Now, write the newly added SchemaObject into the schemaPartition
293         try
294         {
295             wrapped.add( addContext );
296         }
297         catch ( LdapException e )
298         {
299             // If something went wrong, we have to unregister the schemaObject
300             // from the registries
301             // TODO : deregister the newly added element.
302             throw e;
303         }
304 
305         updateSchemaModificationAttributes( addContext );
306     }
307 
308 
309     /**
310      * {@inheritDoc}
311      */
312     public int getChildCount( DeleteOperationContext deleteContext ) throws LdapException
313     {
314         try
315         {
316             Dn dn = deleteContext.getDn();
317             SearchRequest searchRequest = new SearchRequestImpl();
318             searchRequest.setBase( dn );
319             ExprNode node = new PresenceNode( objectClassAT );
320             searchRequest.setFilter( node );
321             searchRequest.setTypesOnly( true );
322             searchRequest.setScope( SearchScope.ONELEVEL );
323 
324             SearchOperationContextceptor/context/SearchOperationContext.html#SearchOperationContext">SearchOperationContext searchContext = new SearchOperationContext( deleteContext.getSession(),
325                 searchRequest );
326             searchContext.setPartition( this );
327             searchContext.setTransaction( deleteContext.getTransaction() );
328 
329             EntryFilteringCursor cursor = wrapped.search( searchContext );
330 
331             cursor.beforeFirst();
332             int nbEntry = 0;
333 
334             while ( cursor.next() && ( nbEntry < 2 ) )
335             {
336                 nbEntry++;
337             }
338 
339             cursor.close();
340 
341             return nbEntry;
342         }
343         catch ( Exception e )
344         {
345             LOG.warn( "Failed to get the children of entry {}", deleteContext.getDn() );
346             return 0;
347         }
348     }
349 
350 
351     /**
352      * {@inheritDoc}
353      */
354     public Entry delete( DeleteOperationContext deleteContext ) throws LdapException
355     {
356         boolean cascade = deleteContext.hasRequestControl( Cascade.OID );
357 
358         // We have to check if the entry we want to delete has children, or not
359         int nbChild = getChildCount( deleteContext );
360 
361         if ( nbChild > 1 )
362         {
363             throw new LdapContextNotEmptyException( "There are children under the entry " + deleteContext.getDn() );
364         }
365 
366         // The SchemaObject always exist when we reach this method.
367         synchronizer.delete( deleteContext, cascade );
368         Entry deletedEntry = null;
369 
370         try
371         {
372             deletedEntry = wrapped.delete( deleteContext );
373         }
374         catch ( LdapException e )
375         {
376             // TODO : If something went wrong, what should we do here ?
377             throw e;
378         }
379 
380         updateSchemaModificationAttributes( deleteContext );
381 
382         return deletedEntry;
383     }
384 
385 
386     /**
387      * {@inheritDoc}
388      */
389     public boolean hasEntry( HasEntryOperationContext hasEntryContext ) throws LdapException
390     {
391         return wrapped.hasEntry( hasEntryContext );
392     }
393 
394 
395     /**
396      * {@inheritDoc}
397      */
398     public void modify( ModifyOperationContext modifyContext ) throws LdapException
399     {
400         Entry entry = modifyContext.getEntry();
401 
402         if ( entry == null )
403         {
404             LookupOperationContextnterceptor/context/LookupOperationContext.html#LookupOperationContext">LookupOperationContext lookupCtx = new LookupOperationContext( modifyContext.getSession(),
405                 modifyContext.getDn() );
406             lookupCtx.setPartition( this );
407             lookupCtx.setTransaction( modifyContext.getTransaction() );
408             entry = wrapped.lookup( lookupCtx );
409             modifyContext.setEntry( entry );
410         }
411 
412         Entry targetEntry = SchemaUtils.getTargetEntry( modifyContext.getModItems(), entry );
413 
414         boolean cascade = modifyContext.hasRequestControl( Cascade.OID );
415 
416         boolean hasModification = synchronizer.modify( modifyContext, targetEntry, cascade );
417 
418         if ( hasModification )
419         {
420             wrapped.modify( modifyContext );
421         }
422 
423         if ( !modifyContext.getDn().equals( schemaModificationDN ) )
424         {
425             updateSchemaModificationAttributes( modifyContext );
426         }
427     }
428 
429 
430     /**
431      * {@inheritDoc}
432      */
433     public void move( MoveOperationContext moveContext ) throws LdapException
434     {
435         boolean cascade = moveContext.hasRequestControl( Cascade.OID );
436 
437         CoreSession session = moveContext.getSession();
438         LookupOperationContextceptor/context/LookupOperationContext.html#LookupOperationContext">LookupOperationContext lookupContext = new LookupOperationContext( session, moveContext.getDn(),
439             SchemaConstants.ALL_ATTRIBUTES_ARRAY );
440         lookupContext.setPartition( this );
441         lookupContext.setTransaction( moveContext.getTransaction() );
442 
443         Entry entry = session.getDirectoryService().getPartitionNexus().lookup( lookupContext );
444         synchronizer.move( moveContext, entry, cascade );
445         wrapped.move( moveContext );
446         updateSchemaModificationAttributes( moveContext );
447     }
448 
449 
450     /**
451      * {@inheritDoc}
452      */
453     public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
454     {
455         boolean cascade = moveAndRenameContext.hasRequestControl( Cascade.OID );
456         CoreSession session = moveAndRenameContext.getSession();
457         LookupOperationContextceptor/context/LookupOperationContext.html#LookupOperationContext">LookupOperationContext lookupContext = new LookupOperationContext( session, moveAndRenameContext.getDn(),
458             SchemaConstants.ALL_ATTRIBUTES_ARRAY );
459         lookupContext.setPartition( this );
460         lookupContext.setTransaction( moveAndRenameContext.getTransaction() );
461 
462         Entry entry = session.getDirectoryService().getPartitionNexus().lookup( lookupContext );
463         synchronizer.moveAndRename( moveAndRenameContext, entry, cascade );
464         wrapped.moveAndRename( moveAndRenameContext );
465         updateSchemaModificationAttributes( moveAndRenameContext );
466     }
467 
468 
469     /**
470      * {@inheritDoc}
471      */
472     public void rename( RenameOperationContext renameContext ) throws LdapException
473     {
474         boolean cascade = renameContext.hasRequestControl( Cascade.OID );
475 
476         // First update the registries
477         synchronizer.rename( renameContext, cascade );
478 
479         // Update the schema partition
480         wrapped.rename( renameContext );
481 
482         // Update the SSSE operational attributes
483         updateSchemaModificationAttributes( renameContext );
484     }
485 
486 
487     /**
488      * {@inheritDoc}
489      */
490     public EntryFilteringCursor search( SearchOperationContext searchContext ) throws LdapException
491     {
492         return wrapped.search( searchContext );
493     }
494 
495 
496     /**
497      * {@inheritDoc}
498      */
499     public void unbind( UnbindOperationContext unbindContext ) throws LdapException
500     {
501         wrapped.unbind( unbindContext );
502     }
503 
504 
505     /**
506      * {@inheritDoc}
507      */
508     public Entry lookup( LookupOperationContext lookupContext ) throws LdapException
509     {
510         return wrapped.lookup( lookupContext );
511     }
512 
513 
514     /**
515      * Updates the schemaModifiersName and schemaModifyTimestamp attributes of
516      * the schemaModificationAttributes entry for the global schema at
517      * ou=schemaModifications,ou=schema.  This entry is hardcoded at that
518      * position for now.
519      * 
520      * The current time is used to set the timestamp and the Dn of current user
521      * is set for the modifiersName.
522      * 
523      * @throws LdapException if the update fails
524      */
525     private void updateSchemaModificationAttributes( OperationContext opContext ) throws LdapException
526     {
527         String modifiersName = opContext.getSession().getEffectivePrincipal().getName();
528         String modifyTimestamp = DateUtils.getGeneralizedTime( opContext.getSession().getDirectoryService().getTimeProvider() );
529 
530         List<Modification> mods = new ArrayList<>( 2 );
531 
532         mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, new DefaultAttribute(
533             ApacheSchemaConstants.SCHEMA_MODIFY_TIMESTAMP_AT, schemaManager
534                 .lookupAttributeTypeRegistry( ApacheSchemaConstants.SCHEMA_MODIFY_TIMESTAMP_AT ), modifyTimestamp ) ) );
535 
536         mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, new DefaultAttribute(
537             ApacheSchemaConstants.SCHEMA_MODIFIERS_NAME_AT, schemaManager
538                 .lookupAttributeTypeRegistry( ApacheSchemaConstants.SCHEMA_MODIFIERS_NAME_AT ), modifiersName ) ) );
539 
540         // operation reached this level means all the necessary ACI and other checks should
541         // have been done, so we can perform the below modification directly on the partition nexus
542         // without using a a bypass list
543         CoreSession session = opContext.getSession();
544         ModifyOperationContextceptor/context/ModifyOperationContext.html#ModifyOperationContext">ModifyOperationContext modifyContext = new ModifyOperationContext( session, schemaModificationDN, mods );
545         modifyContext.setPartition( this );
546         modifyContext.setTransaction( opContext.getTransaction() );
547         
548         session.getDirectoryService().getPartitionNexus().modify( modifyContext );
549     }
550 
551 
552     @Override
553     public String getContextCsn( PartitionTxn partitionTxn )
554     {
555         return wrapped.getContextCsn( partitionTxn );
556     }
557 
558 
559     @Override
560     public void saveContextCsn( PartitionTxn partitionTxn ) throws LdapException
561     {
562         wrapped.saveContextCsn( partitionTxn );
563     }
564 
565 
566     /**
567      * @see Object#toString()
568      */
569     public String toString()
570     {
571         return "Partition : " + SCHEMA_ID;
572     }
573     
574     
575     /**
576      * Return the number of children and subordinates for a given entry
577      *
578      * @param partitionTxn The transaction
579      * @param entry The entry we want the subordinates for
580      * @return The Subordinate instance that contains the values.
581      * @throws LdapException If we had an issue while processing the request
582      */
583     public Subordinates getSubordinates( PartitionTxn partitionTxn, Entry entry ) throws LdapException
584     {
585         return new Subordinates();
586     }
587 
588 
589     @Override
590     public PartitionReadTxn beginReadTransaction()
591     {
592         return new PartitionReadTxn();
593     }
594 
595 
596     @Override
597     public PartitionWriteTxn beginWriteTransaction()
598     {
599         return new PartitionWriteTxn();
600     }
601 }