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     */
020    package org.apache.directory.server.core.exception;
021    
022    
023    import java.util.List;
024    
025    import org.apache.commons.collections.map.LRUMap;
026    import org.apache.directory.server.core.DirectoryService;
027    import org.apache.directory.server.core.entry.ClonedServerEntry;
028    import org.apache.directory.server.core.filtering.BaseEntryFilteringCursor;
029    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
030    import org.apache.directory.server.core.interceptor.BaseInterceptor;
031    import org.apache.directory.server.core.interceptor.NextInterceptor;
032    import org.apache.directory.server.core.interceptor.context.AddOperationContext;
033    import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
034    import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
035    import org.apache.directory.server.core.interceptor.context.GetSuffixOperationContext;
036    import org.apache.directory.server.core.interceptor.context.ListOperationContext;
037    import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
038    import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
039    import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
040    import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
041    import org.apache.directory.server.core.interceptor.context.OperationContext;
042    import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
043    import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
044    import org.apache.directory.server.core.partition.ByPassConstants;
045    import org.apache.directory.server.core.partition.Partition;
046    import org.apache.directory.server.core.partition.PartitionNexus;
047    import org.apache.directory.server.i18n.I18n;
048    import org.apache.directory.shared.ldap.constants.SchemaConstants;
049    import org.apache.directory.shared.ldap.cursor.EmptyCursor;
050    import org.apache.directory.shared.ldap.entry.EntryAttribute;
051    import org.apache.directory.shared.ldap.entry.Modification;
052    import org.apache.directory.shared.ldap.entry.ModificationOperation;
053    import org.apache.directory.shared.ldap.entry.ServerEntry;
054    import org.apache.directory.shared.ldap.entry.Value;
055    import org.apache.directory.shared.ldap.exception.LdapAliasException;
056    import org.apache.directory.shared.ldap.exception.LdapAttributeInUseException;
057    import org.apache.directory.shared.ldap.exception.LdapContextNotEmptyException;
058    import org.apache.directory.shared.ldap.exception.LdapEntryAlreadyExistsException;
059    import org.apache.directory.shared.ldap.exception.LdapNoSuchObjectException;
060    import org.apache.directory.shared.ldap.exception.LdapUnwillingToPerformException;
061    import org.apache.directory.shared.ldap.message.ResultCodeEnum;
062    import org.apache.directory.shared.ldap.name.DN;
063    
064    
065    /**
066     * An {@link org.apache.directory.server.core.interceptor.Interceptor} that detects any operations that breaks integrity
067     * of {@link Partition} and terminates the current invocation chain by
068     * throwing a {@link Exception}. Those operations include when an entry
069     * already exists at a DN and is added once again to the same DN.
070     *
071     * @org.apache.xbean.XBean
072     *
073     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
074     * @version $Rev: 927839 $
075     */
076    public class ExceptionInterceptor extends BaseInterceptor
077    {
078        private PartitionNexus nexus;
079        private DirectoryService directoryService;
080        private DN subschemSubentryDn;
081    
082        
083        /**
084         * A cache to store entries which are not aliases. 
085         * It's a speedup, we will be able to avoid backend lookups.
086         * 
087         * Note that the backend also use a cache mechanism, but for performance gain, it's good 
088         * to manage a cache here. The main problem is that when a user modify the parent, we will
089         * have to update it at three different places :
090         * - in the backend,
091         * - in the partition cache,
092         * - in this cache.
093         * 
094         * The update of the backend and partition cache is already correctly handled, so we will
095         * just have to offer an access to refresh the local cache. This should be done in 
096         * delete, modify and move operations.
097         * 
098         * We need to be sure that frequently used DNs are always in cache, and not discarded.
099         * We will use a LRU cache for this purpose. 
100         */ 
101        private final LRUMap notAliasCache = new LRUMap( DEFAULT_CACHE_SIZE );
102    
103        /** Declare a default for this cache. 100 entries seems to be enough */
104        private static final int DEFAULT_CACHE_SIZE = 100;
105    
106        
107        /**
108         * Creates an interceptor that is also the exception handling service.
109         */
110        public ExceptionInterceptor()
111        {
112        }
113    
114    
115        public void init( DirectoryService directoryService ) throws Exception
116        {
117            this.directoryService = directoryService;
118            nexus = directoryService.getPartitionNexus();
119            Value<?> attr = nexus.getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
120            subschemSubentryDn = new DN( attr.getString() );
121            subschemSubentryDn.normalize( directoryService.getSchemaManager().getNormalizerMapping() );
122        }
123    
124    
125        public void destroy()
126        {
127        }
128    
129        /**
130         * In the pre-invocation state this interceptor method checks to see if the entry to be added already exists.  If it
131         * does an exception is raised.
132         */
133        public void add( NextInterceptor nextInterceptor, AddOperationContext opContext )
134            throws Exception
135        {
136            DN name = opContext.getDn();
137            
138            if ( subschemSubentryDn.getNormName().equals( name.getNormName() ) )
139            {
140                throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_249 ) );
141            }
142            
143            // check if the entry already exists
144            if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), name ) ) )
145            {
146                LdapEntryAlreadyExistsException ne = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_250, name.getName() ) );
147                //ne.setResolvedName( new DN( name.getName() ) );
148                throw ne;
149            }
150            
151            DN suffix = nexus.getSuffix( new GetSuffixOperationContext( this.directoryService.getAdminSession(), 
152                name ) );
153            
154            // we're adding the suffix entry so just ignore stuff to mess with the parent
155            if ( suffix.equals( name ) )
156            {
157                nextInterceptor.add( opContext );
158                return;
159            }
160            
161            DN parentDn = ( DN ) name.clone();
162            parentDn.remove( name.size() - 1 );
163            
164            // check if we're trying to add to a parent that is an alias
165            boolean notAnAlias;
166            
167            synchronized( notAliasCache )
168            {
169                notAnAlias = notAliasCache.containsKey( parentDn.getNormName() );
170            }
171            
172            if ( ! notAnAlias )
173            {
174                // We don't know if the parent is an alias or not, so we will launch a 
175                // lookup, and update the cache if it's not an alias
176                ClonedServerEntry attrs;
177                
178                try
179                {
180                    attrs = opContext.lookup( parentDn, ByPassConstants.LOOKUP_BYPASS );
181                }
182                catch ( Exception e )
183                {
184                    LdapNoSuchObjectException e2 = new LdapNoSuchObjectException( I18n.err( I18n.ERR_251, 
185                        parentDn.getName() ) );
186                    //e2.setResolvedName( new DN( nexus.getMatchedName( 
187                      //  new GetMatchedNameOperationContext( opContext.getSession(), parentDn ) ).getName() ) );
188                    throw e2;
189                }
190                
191                EntryAttribute objectClass = attrs.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT );
192                
193                if ( objectClass.contains( SchemaConstants.ALIAS_OC ) )
194                {
195                    String msg = I18n.err( I18n.ERR_252, name.getName() );
196                    LdapAliasException e = new LdapAliasException( msg );
197                    //e.setResolvedName( new DN( parentDn.getName() ) );
198                    throw e;
199                }
200                else
201                {
202                    synchronized ( notAliasCache )
203                    {
204                        notAliasCache.put( parentDn.getNormName(), parentDn );
205                    }
206                }
207            }
208    
209            nextInterceptor.add( opContext );
210        }
211    
212    
213        /**
214         * Checks to make sure the entry being deleted exists, and has no children, otherwise throws the appropriate
215         * LdapException.
216         */
217        public void delete( NextInterceptor nextInterceptor, DeleteOperationContext opContext ) throws Exception
218        {
219            DN name = opContext.getDn();
220            
221            if ( name.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
222            {
223                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
224                    I18n.err( I18n.ERR_253, subschemSubentryDn ) );
225            }
226            
227            // check if entry to delete exists
228            String msg = "Attempt to delete non-existant entry: ";
229            assertHasEntry( nextInterceptor, opContext, msg, name );
230    
231            // check if entry to delete has children (only leaves can be deleted)
232            boolean hasChildren = false;
233            EntryFilteringCursor list = nextInterceptor.list( new ListOperationContext( opContext.getSession(), name ) );
234            
235            if ( list.next() )
236            {
237                hasChildren = true;
238            }
239    
240            list.close();
241            
242            if ( hasChildren )
243            {
244                LdapContextNotEmptyException e = new LdapContextNotEmptyException();
245                //e.setResolvedName( new DN( name.getName() ) );
246                throw e;
247            }
248    
249            synchronized( notAliasCache )
250            {
251                if ( notAliasCache.containsKey( name.getNormName() ) )
252                {
253                    notAliasCache.remove( name.getNormName() );
254                }
255            }
256            
257            nextInterceptor.delete( opContext );
258        }
259    
260    
261        /**
262         * Checks to see the base being searched exists, otherwise throws the appropriate LdapException.
263         */
264        public EntryFilteringCursor list( NextInterceptor nextInterceptor, ListOperationContext opContext ) throws Exception
265        {
266            if ( opContext.getDn().getNormName().equals( subschemSubentryDn.getNormName() ) )
267            {
268                // there is nothing under the schema subentry
269                return new BaseEntryFilteringCursor( new EmptyCursor<ServerEntry>(), opContext );
270            }
271            
272            // check if entry to search exists
273            String msg = "Attempt to search under non-existant entry: ";
274            assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
275    
276            return nextInterceptor.list( opContext );
277        }
278    
279    
280        /**
281         * Checks to see the base being searched exists, otherwise throws the appropriate LdapException.
282         */
283        public ClonedServerEntry lookup( NextInterceptor nextInterceptor, LookupOperationContext opContext ) throws Exception
284        {
285            if ( opContext.getDn().getNormName().equals( subschemSubentryDn.getNormName() ) )
286            {
287                return nexus.getRootDSE( null );
288            }
289            
290            // check if entry to lookup exists
291            String msg = "Attempt to lookup non-existant entry: ";
292            assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
293    
294            return nextInterceptor.lookup( opContext );
295        }
296    
297    
298        /**
299         * Checks to see the entry being modified exists, otherwise throws the appropriate LdapException.
300         */
301        public void modify( NextInterceptor nextInterceptor, ModifyOperationContext opContext )
302            throws Exception
303        {
304            // check if entry to modify exists
305            String msg = "Attempt to modify non-existant entry: ";
306    
307            // handle operations against the schema subentry in the schema service
308            // and never try to look it up in the nexus below
309            if ( opContext.getDn().getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
310            {
311                nextInterceptor.modify( opContext );
312                return;
313            }
314            
315            assertHasEntry( nextInterceptor, opContext, msg, opContext.getDn() );
316    
317            ServerEntry entry = opContext.lookup( opContext.getDn(), ByPassConstants.LOOKUP_BYPASS );
318            List<Modification> items = opContext.getModItems();
319    
320            for ( Modification item : items )
321            {
322                if ( item.getOperation() == ModificationOperation.ADD_ATTRIBUTE )
323                {
324                    EntryAttribute modAttr = item.getAttribute();
325                    EntryAttribute entryAttr = entry.get( modAttr.getId() );
326    
327                    if ( entryAttr != null )
328                    {
329                        for ( Value<?> value:modAttr )
330                        {
331                            if ( entryAttr.contains( value ) )
332                            {
333                                throw new LdapAttributeInUseException( I18n.err( I18n.ERR_254, value,
334                                    modAttr.getId() ) );
335                            }
336                        }
337                    }
338                }
339            }
340    
341            // Let's assume that the new modified entry may be an alias,
342            // but we don't want to check that now...
343            // We will simply remove the DN from the NotAlias cache.
344            // It would be smarter to check the modified attributes, but
345            // it would also be more complex.
346            synchronized( notAliasCache )
347            {
348                if ( notAliasCache.containsKey( opContext.getDn().getNormName() ) )
349                {
350                    notAliasCache.remove( opContext.getDn().getNormName() );
351                }
352            }
353    
354            nextInterceptor.modify( opContext );
355        }
356    
357        /**
358         * Checks to see the entry being renamed exists, otherwise throws the appropriate LdapException.
359         */
360        public void rename( NextInterceptor nextInterceptor, RenameOperationContext opContext )
361            throws Exception
362        {
363            DN dn = opContext.getDn();
364            
365            if ( dn.equals( subschemSubentryDn ) )
366            {
367                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_255, subschemSubentryDn,
368                        subschemSubentryDn ) );
369            }
370            
371            // Check to see if the renamed entry exists
372            if ( opContext.getEntry() == null )
373            {
374                // This is a nonsense : we can't rename an entry which does not exist
375                // on the server
376                LdapNoSuchObjectException ldnfe;
377                ldnfe = new LdapNoSuchObjectException( I18n.err( I18n.ERR_256, dn.getName() ) );
378                //ldnfe.setResolvedName( new DN( dn.getName() ) );
379                throw ldnfe;
380            }
381            
382            // check to see if target entry exists
383            DN newDn = opContext.getNewDn();
384            
385            if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), newDn ) ) )
386            {
387                LdapEntryAlreadyExistsException e;
388                e = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_257, newDn.getName() ) );
389                //e.setResolvedName( new DN( newDn.getName() ) );
390                throw e;
391            }
392    
393            // Remove the previous entry from the notAnAlias cache
394            synchronized( notAliasCache )
395            {
396                if ( notAliasCache.containsKey( dn.getNormName() ) )
397                {
398                    notAliasCache.remove( dn.getNormName() );
399                }
400            }
401    
402            nextInterceptor.rename( opContext );
403        }
404    
405    
406        /**
407         * Checks to see the entry being moved exists, and so does its parent, otherwise throws the appropriate
408         * LdapException.
409         */
410        public void move( NextInterceptor nextInterceptor, MoveOperationContext opContext ) throws Exception
411        {
412            DN oriChildName = opContext.getDn();
413            DN newParentName = opContext.getParent();
414            
415            if ( oriChildName.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
416            {
417                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258, subschemSubentryDn,
418                        subschemSubentryDn ) );
419            }
420            
421            // check if child to move exists
422            String msg = "Attempt to move to non-existant parent: ";
423            assertHasEntry( nextInterceptor, opContext, msg, oriChildName );
424    
425            // check if parent to move to exists
426            msg = "Attempt to move to non-existant parent: ";
427            assertHasEntry( nextInterceptor, opContext, msg, newParentName );
428    
429            // check to see if target entry exists
430            String rdn = oriChildName.get( oriChildName.size() - 1 );
431            DN target = ( DN ) newParentName.clone();
432            target.add( rdn );
433            
434            if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), target ) ) )
435            {
436                // we must calculate the resolved name using the user provided Rdn value
437                String upRdn = new DN( oriChildName.getName() ).get( oriChildName.size() - 1 );
438                DN upTarget = ( DN ) newParentName.clone();
439                upTarget.add( upRdn );
440    
441                LdapEntryAlreadyExistsException e;
442                e = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_257, upTarget.getName() ) );
443                //e.setResolvedName( new DN( upTarget.getName() ) );
444                throw e;
445            }
446    
447            // Remove the original entry from the NotAlias cache, if needed
448            synchronized( notAliasCache )
449            {
450                if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
451                {
452                    notAliasCache.remove( oriChildName.getNormName() );
453                }
454            }
455                    
456            nextInterceptor.move( opContext );
457        }
458    
459    
460        /**
461         * Checks to see the entry being moved exists, and so does its parent, otherwise throws the appropriate
462         * LdapException.
463         */
464        public void moveAndRename( NextInterceptor nextInterceptor, MoveAndRenameOperationContext opContext ) throws Exception
465        {
466            DN oriChildName = opContext.getDn();
467            DN parent = opContext.getParent();
468    
469            if ( oriChildName.getNormName().equalsIgnoreCase( subschemSubentryDn.getNormName() ) )
470            {
471                throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258, subschemSubentryDn,
472                        subschemSubentryDn ) );
473            }
474            
475            // check if child to move exists
476            String msg = "Attempt to move to non-existant parent: ";
477            assertHasEntry( nextInterceptor, opContext, msg, oriChildName );
478    
479            // check if parent to move to exists
480            msg = "Attempt to move to non-existant parent: ";
481            assertHasEntry( nextInterceptor, opContext, msg, parent );
482    
483            // check to see if target entry exists
484            DN target = ( DN ) parent.clone();
485            target.add( opContext.getNewRdn() );
486    
487            if ( nextInterceptor.hasEntry( new EntryOperationContext( opContext.getSession(), target ) ) )
488            {
489                // we must calculate the resolved name using the user provided Rdn value
490                DN upTarget = ( DN ) parent.clone();
491                upTarget.add( opContext.getNewRdn() );
492    
493                LdapEntryAlreadyExistsException e;
494                e = new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_257, upTarget.getName() ) );
495                //e.setResolvedName( new DN( upTarget.getName() ) );
496                throw e;
497            }
498    
499            // Remove the original entry from the NotAlias cache, if needed
500            synchronized( notAliasCache )
501            {
502                if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
503                {
504                    notAliasCache.remove( oriChildName.getNormName() );
505                }
506            }
507            
508            nextInterceptor.moveAndRename( opContext );
509        }
510    
511    
512        /**
513         * Checks to see the entry being searched exists, otherwise throws the appropriate LdapException.
514         */
515        public EntryFilteringCursor search( NextInterceptor nextInterceptor, SearchOperationContext opContext ) throws Exception
516        {
517            DN base = opContext.getDn();
518    
519            try
520            {
521                EntryFilteringCursor cursor =  nextInterceptor.search( opContext );
522                
523                if ( ! cursor.next() )
524                {
525                    if ( !base.isEmpty() && !( subschemSubentryDn.getNormName() ).equalsIgnoreCase( base.getNormName() ) )
526                    {
527                        // We just check that the entry exists only if we didn't found any entry
528                        assertHasEntry( nextInterceptor, opContext, "Attempt to search under non-existant entry:" , base );
529                    }
530                }
531    
532                return cursor;
533            }
534            catch ( Exception ne )
535            {
536                String msg = I18n.err( I18n.ERR_259 );
537                assertHasEntry( nextInterceptor, opContext, msg, base );
538                throw ne;
539            }
540        }
541    
542    
543        /**
544         * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is
545         * used to set the before exception on the invocation - eventually the exception is thrown.
546         *
547         * @param msg        the message to prefix to the distinguished name for explanation
548         * @param dn         the distinguished name of the entry that is asserted
549         * @throws Exception if the entry does not exist
550         * @param nextInterceptor the next interceptor in the chain
551         */
552        private void assertHasEntry( NextInterceptor nextInterceptor, OperationContext opContext, 
553            String msg, DN dn ) throws Exception
554        {
555            if ( subschemSubentryDn.getNormName().equals( dn.getNormName() ) )
556            {
557                return;
558            }
559            
560            if ( ! opContext.hasEntry( dn, ByPassConstants.HAS_ENTRY_BYPASS ) )
561            {
562                LdapNoSuchObjectException e;
563    
564                if ( msg != null )
565                {
566                    e = new LdapNoSuchObjectException( msg + dn.getName() );
567                }
568                else
569                {
570                    e = new LdapNoSuchObjectException( dn.getName() );
571                }
572    
573                //e.setResolvedName( 
574                //    new DN( 
575                //        opContext.getSession().getDirectoryService().getOperationManager().getMatchedName( 
576                //            new GetMatchedNameOperationContext( opContext.getSession(), dn ) ).getName() ) );
577                throw e;
578            }
579        }
580    }