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    /**
256     * {@inheritDoc}
257     */
258    @Override
259    public void move( MoveOperationContext moveContext ) throws LdapException
260    {
261        Dn oriChildName = moveContext.getDn();
262
263        if ( oriChildName.equals( subschemSubentryDn ) )
264        {
265            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258,
266                subschemSubentryDn, subschemSubentryDn ) );
267        }
268
269        next( moveContext );
270
271        // Remove the original entry from the NotAlias cache, if needed
272        synchronized ( notAliasCache )
273        {
274            if ( notAliasCache.containsKey( oriChildName.getNormName() ) )
275            {
276                notAliasCache.remove( oriChildName.getNormName() );
277            }
278        }
279    }
280
281
282    /**
283     * {@inheritDoc}
284     */
285    @Override
286    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
287    {
288        Dn oldDn = moveAndRenameContext.getDn();
289
290        // Don't allow M&R in the SSSE
291        if ( oldDn.getNormName().equals( subschemSubentryDn.getNormName() ) )
292        {
293            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_258,
294                subschemSubentryDn, subschemSubentryDn ) );
295        }
296
297        // Remove the original entry from the NotAlias cache, if needed
298        synchronized ( notAliasCache )
299        {
300            if ( notAliasCache.containsKey( oldDn.getNormName() ) )
301            {
302                notAliasCache.remove( oldDn.getNormName() );
303            }
304        }
305
306        next( moveAndRenameContext );
307    }
308
309
310    /**
311     * {@inheritDoc}
312     */
313    @Override
314    public void rename( RenameOperationContext renameContext ) throws LdapException
315    {
316        Dn dn = renameContext.getDn();
317
318        if ( dn.equals( subschemSubentryDn ) )
319        {
320            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, I18n.err( I18n.ERR_255,
321                subschemSubentryDn, subschemSubentryDn ) );
322        }
323
324        // check to see if target entry exists
325        Dn newDn = renameContext.getNewDn();
326
327        HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( renameContext.getSession(), newDn );
328        hasEntryContext.setPartition( renameContext.getPartition() );
329        hasEntryContext.setTransaction( renameContext.getTransaction() );
330
331        if ( nexus.hasEntry( hasEntryContext ) )
332        {
333            // Ok, the target entry already exists.
334            // If the target entry has the same name than the modified entry, it's a rename on itself,
335            // we want to allow this.
336            if ( !newDn.equals( dn ) )
337            {
338                throw new LdapEntryAlreadyExistsException( I18n.err( I18n.ERR_250_ENTRY_ALREADY_EXISTS, newDn.getName() ) );
339            }
340        }
341
342        // Remove the previous entry from the notAnAlias cache
343        synchronized ( notAliasCache )
344        {
345            if ( notAliasCache.containsKey( dn.getNormName() ) )
346            {
347                notAliasCache.remove( dn.getNormName() );
348            }
349        }
350
351        next( renameContext );
352    }
353
354
355    /**
356     * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is
357     * used to set the before exception on the invocation - eventually the exception is thrown.
358     *
359     * @param msg        the message to prefix to the distinguished name for explanation
360     * @throws Exception if the entry does not exist
361     * @param nextInterceptor the next interceptor in the chain
362     */
363    private void assertHasEntry( OperationContext opContext, String msg ) throws LdapException
364    {
365        Dn dn = opContext.getDn();
366
367        if ( subschemSubentryDn.equals( dn ) )
368        {
369            return;
370        }
371
372        if ( opContext.getEntry() == null )
373        {
374            LdapNoSuchObjectException e;
375
376            if ( msg != null )
377            {
378                e = new LdapNoSuchObjectException( msg + dn.getName() );
379            }
380            else
381            {
382                e = new LdapNoSuchObjectException( dn.getName() );
383            }
384
385            throw e;
386        }
387    }
388
389    /**
390     * Asserts that an entry is present and as a side effect if it is not, creates a LdapNoSuchObjectException, which is
391     * used to set the before exception on the invocation - eventually the exception is thrown.
392     *
393     * @param msg        the message to prefix to the distinguished name for explanation
394     * @param dn         the distinguished name of the entry that is asserted
395     * @throws Exception if the entry does not exist
396     * @param nextInterceptor the next interceptor in the chain
397     *
398    private void assertHasEntry( OperationContext opContext, String msg, Dn dn ) throws LdapException
399    {
400        if ( subschemSubentryDn.equals( dn ) )
401        {
402            return;
403        }
404
405        if ( !opContext.hasEntry( dn, ByPassConstants.HAS_ENTRY_BYPASS ) )
406        {
407            LdapNoSuchObjectException e;
408
409            if ( msg != null )
410            {
411                e = new LdapNoSuchObjectException( msg + dn.getName() );
412            }
413            else
414            {
415                e = new LdapNoSuchObjectException( dn.getName() );
416            }
417
418            throw e;
419        }
420    }*/
421}