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.exception;
021
022
023import org.apache.commons.collections4.map.LRUMap;
024import org.apache.directory.api.ldap.model.constants.SchemaConstants;
025import org.apache.directory.api.ldap.model.entry.Attribute;
026import org.apache.directory.api.ldap.model.entry.Entry;
027import org.apache.directory.api.ldap.model.entry.Value;
028import org.apache.directory.api.ldap.model.exception.LdapAliasException;
029import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
030import org.apache.directory.api.ldap.model.exception.LdapException;
031import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException;
032import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
033import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
034import org.apache.directory.api.ldap.model.name.Dn;
035import org.apache.directory.server.core.api.CoreSession;
036import org.apache.directory.server.core.api.DirectoryService;
037import org.apache.directory.server.core.api.InterceptorEnum;
038import org.apache.directory.server.core.api.entry.ClonedServerEntry;
039import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
040import org.apache.directory.server.core.api.interceptor.Interceptor;
041import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
042import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
043import org.apache.directory.server.core.api.interceptor.context.HasEntryOperationContext;
044import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
045import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
046import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
047import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
048import org.apache.directory.server.core.api.interceptor.context.OperationContext;
049import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
050import org.apache.directory.server.core.api.partition.Partition;
051import org.apache.directory.server.core.api.partition.PartitionNexus;
052import org.apache.directory.server.i18n.I18n;
053
054
055/**
056 * An {@link Interceptor} that detects any operations that breaks integrity
057 * of {@link Partition} and terminates the current invocation chain by
058 * throwing a {@link Exception}. Those operations include when an entry
059 * already exists at a Dn and is added once again to the same Dn.
060 *
061 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
062 */
063public class ExceptionInterceptor extends BaseInterceptor
064{
065    private PartitionNexus nexus;
066    private Dn subschemSubentryDn;
067
068    /**
069     * A cache to store entries which are not aliases.
070     * It's a speedup, we will be able to avoid backend lookups.
071     *
072     * Note that the backend also use a cache mechanism, but for performance gain, it's good
073     * to manage a cache here. The main problem is that when a user modify the parent, we will
074     * have to update it at three different places :
075     * - in the backend,
076     * - in the partition cache,
077     * - in this cache.
078     *
079     * The update of the backend and partition cache is already correctly handled, so we will
080     * just have to offer an access to refresh the local cache. This should be done in
081     * delete, modify and move operations.
082     *
083     * We need to be sure that frequently used DNs are always in cache, and not discarded.
084     * We will use a LRU cache for this purpose.
085     */
086    private final LRUMap notAliasCache = new LRUMap( DEFAULT_CACHE_SIZE );
087
088    /** Declare a default for this cache. 100 entries seems to be enough */
089    private static final int DEFAULT_CACHE_SIZE = 100;
090
091
092    /**
093     * Creates an interceptor that is also the exception handling service.
094     */
095    public ExceptionInterceptor()
096    {
097        super( InterceptorEnum.EXCEPTION_INTERCEPTOR );
098    }
099
100
101    /**
102     * {@inheritDoc}
103     */
104    @Override
105    public void init( DirectoryService directoryService ) throws LdapException
106    {
107        super.init( directoryService );
108        nexus = directoryService.getPartitionNexus();
109        Value attr = nexus.getRootDseValue( directoryService.getAtProvider().getSubschemaSubentry() );
110        subschemSubentryDn = dnFactory.create( attr.getString() );
111    }
112
113
114    /**
115     * In the pre-invocation state this interceptor method checks to see if the entry to be added already exists.  If it
116     * does an exception is raised.
117     */
118    @Override
119    public void add( AddOperationContext addContext ) throws LdapException
120    {
121        Dn name = addContext.getDn();
122
123        if ( subschemSubentryDn.equals( name ) )
124        {
125            throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_249 ) );
126        }
127
128        Dn suffix = nexus.getSuffixDn( name );
129
130        // we're adding the suffix entry so just ignore stuff to mess with the parent
131        if ( suffix.equals( name ) )
132        {
133            next( addContext );
134            return;
135        }
136
137        Dn parentDn = name.getParent();
138
139        // check if we're trying to add to a parent that is an alias
140        boolean notAnAlias;
141
142        synchronized ( notAliasCache )
143        {
144            notAnAlias = notAliasCache.containsKey( parentDn.getNormName() );
145        }
146
147        if ( !notAnAlias )
148        {
149            // We don't know if the parent is an alias or not, so we will launch a
150            // lookup, and update the cache if it's not an alias
151            Entry attrs;
152
153            try
154            {
155                CoreSession session = addContext.getSession();
156                LookupOperationContext lookupContext = new LookupOperationContext( session, parentDn,
157                    SchemaConstants.ALL_ATTRIBUTES_ARRAY );
158                lookupContext.setPartition( addContext.getPartition() );
159                lookupContext.setTransaction( addContext.getTransaction() );
160
161                attrs = directoryService.getPartitionNexus().lookup( lookupContext );
162            }
163            catch ( Exception e )
164            {
165                throw new LdapNoSuchObjectException( I18n.err( I18n.ERR_251_PARENT_NOT_FOUND, parentDn.getName() ) );
166            }
167
168            Attribute objectClass = ( ( ClonedServerEntry ) attrs ).getOriginalEntry().get(
169                directoryService.getAtProvider().getObjectClass() );
170
171            if ( objectClass.contains( SchemaConstants.ALIAS_OC ) )
172            {
173                String msg = I18n.err( I18n.ERR_252_ALIAS_WITH_CHILD_NOT_ALLOWED, name.getName(), parentDn.getName() );
174                throw new LdapAliasException( msg );
175            }
176            else
177            {
178                synchronized ( notAliasCache )
179                {
180                    notAliasCache.put( parentDn.getNormName(), parentDn );
181                }
182            }
183        }
184
185        next( addContext );
186    }
187
188
189    /**
190     * Checks to make sure the entry being deleted exists, and has no children, otherwise throws the appropriate
191     * LdapException.
192     */
193    @Override
194    public void delete( DeleteOperationContext deleteContext ) throws LdapException
195    {
196        Dn dn = deleteContext.getDn();
197
198        if ( dn.equals( subschemSubentryDn ) )
199        {
200            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_253,
201                subschemSubentryDn ) );
202        }
203
204        next( deleteContext );
205
206        // Update the alias cache
207        synchronized ( notAliasCache )
208        {
209            if ( notAliasCache.containsKey( dn.getNormName() ) )
210            {
211                notAliasCache.remove( dn.getNormName() );
212            }
213        }
214    }
215
216
217    /**
218     * {@inheritDoc}
219     */
220    @Override
221    public void modify( ModifyOperationContext modifyContext ) throws LdapException
222    {
223        // check if entry to modify exists
224        String msg = "Attempt to modify non-existant entry: ";
225
226        // handle operations against the schema subentry in the schema service
227        // and never try to look it up in the nexus below
228        if ( modifyContext.getDn().equals( subschemSubentryDn ) )
229        {
230            next( modifyContext );
231            return;
232        }
233
234        // Check that the entry we read at the beginning exists. If
235        // not, we will throw an exception here
236        assertHasEntry( modifyContext, msg );
237
238        // Let's assume that the new modified entry may be an alias,
239        // but we don't want to check that now...
240        // We will simply remove the Dn from the NotAlias cache.
241        // It would be smarter to check the modified attributes, but
242        // it would also be more complex.
243        synchronized ( notAliasCache )
244        {
245            if ( notAliasCache.containsKey( modifyContext.getDn().getNormName() ) )
246            {
247                notAliasCache.remove( modifyContext.getDn().getNormName() );
248            }
249        }
250
251        next( modifyContext );
252    }
253    
254    
255    private void checkExistingTarget( OperationContext operationContext, Dn newDn, Dn oldDn ) throws LdapException
256    {
257        // check to see if target entry exists
258        HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( operationContext.getSession(), newDn );
259        hasEntryContext.setPartition( operationContext.getPartition() );
260        hasEntryContext.setTransaction( operationContext.getTransaction() );
261
262        if ( nexus.hasEntry( hasEntryContext ) )
263        {
264            // Ok, the target entry already exists.
265            // If the target entry has the same name than the modified entry, it's a rename on itself,
266            // we want to allow this.
267            if ( !newDn.equals( oldDn ) )
268            {
269                throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_250_ENTRY_ALREADY_EXISTS, newDn.getName() ) );
270            }
271        }
272    }
273
274
275    /**
276     * {@inheritDoc}
277     */
278    @Override
279    public void move( MoveOperationContext moveContext ) throws LdapException
280    {
281        Dn oriChildName = moveContext.getDn();
282
283        if ( oriChildName.equals( subschemSubentryDn ) )
284        {
285            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258,
286                subschemSubentryDn, subschemSubentryDn ) );
287        }
288        
289        // check to see if target entry exists
290        checkExistingTarget( moveContext, moveContext.getNewDn(), oriChildName );
291
292        next( moveContext );
293
294        // Remove the original entry from the NotAlias cache, if needed
295        synchronized ( notAliasCache )
296        {
297            if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
298            {
299                notAliasCache.remove( oriChildName.getNormName() );
300            }
301        }
302    }
303
304
305    /**
306     * {@inheritDoc}
307     */
308    @Override
309    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
310    {
311        Dn oldDn = moveAndRenameContext.getDn();
312
313        // Don't allow M&R in the SSSE
314        if ( oldDn.getNormName().equals( subschemSubentryDn.getNormName() ) )
315        {
316            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258,
317                subschemSubentryDn, subschemSubentryDn ) );
318        }
319        
320        // check to see if target entry exists
321        checkExistingTarget( moveAndRenameContext, moveAndRenameContext.getNewDn(), oldDn );
322
323        // Remove the original entry from the NotAlias cache, if needed
324        synchronized ( notAliasCache )
325        {
326            if ( notAliasCache.containsKey( oldDn.getNormName() ) )
327            {
328                notAliasCache.remove( oldDn.getNormName() );
329            }
330        }
331
332        next( moveAndRenameContext );
333    }
334
335
336    /**
337     * {@inheritDoc}
338     */
339    @Override
340    public void rename( RenameOperationContext renameContext ) throws LdapException
341    {
342        Dn dn = renameContext.getDn();
343
344        if ( dn.equals( subschemSubentryDn ) )
345        {
346            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_255,
347                subschemSubentryDn, subschemSubentryDn ) );
348        }
349
350        // check to see if target entry exists
351        checkExistingTarget( renameContext, renameContext.getNewDn(), dn );
352
353        // Remove the previous entry from the notAnAlias cache
354        synchronized ( notAliasCache )
355        {
356            if ( notAliasCache.containsKey( dn.getNormName() ) )
357            {
358                notAliasCache.remove( dn.getNormName() );
359            }
360        }
361
362        next( renameContext );
363    }
364
365
366    /**
367     * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is
368     * used to set the before exception on the invocation - eventually the exception is thrown.
369     *
370     * @param msg        the message to prefix to the distinguished name for explanation
371     * @throws Exception if the entry does not exist
372     * @param nextInterceptor the next interceptor in the chain
373     */
374    private void assertHasEntry( OperationContext opContext, String msg ) throws LdapException
375    {
376        Dn dn = opContext.getDn();
377
378        if ( subschemSubentryDn.equals( dn ) )
379        {
380            return;
381        }
382
383        if ( opContext.getEntry() == null )
384        {
385            LdapNoSuchObjectException e;
386
387            if ( msg != null )
388            {
389                e = new LdapNoSuchObjectException( msg + dn.getName() );
390            }
391            else
392            {
393                e = new LdapNoSuchObjectException( dn.getName() );
394            }
395
396            throw e;
397        }
398    }
399
400    /**
401     * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is
402     * used to set the before exception on the invocation - eventually the exception is thrown.
403     *
404     * @param msg        the message to prefix to the distinguished name for explanation
405     * @param dn         the distinguished name of the entry that is asserted
406     * @throws Exception if the entry does not exist
407     * @param nextInterceptor the next interceptor in the chain
408     *
409    private void assertHasEntry( OperationContext opContext, String msg, Dn dn ) throws LdapException
410    {
411        if ( subschemSubentryDn.equals( dn ) )
412        {
413            return;
414        }
415
416        if ( !opContext.hasEntry( dn, ByPassConstants.HAS_ENTRY_BYPASS ) )
417        {
418            LdapNoSuchObjectException e;
419
420            if ( msg != null )
421            {
422                e = new LdapNoSuchObjectException( msg + dn.getName() );
423            }
424            else
425            {
426                e = new LdapNoSuchObjectException( dn.getName() );
427            }
428
429            throw e;
430        }
431    }*/
432}