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 */
019package org.apache.directory.server.core.factory;
020
021
022import java.io.File;
023import java.io.FileNotFoundException;
024import java.io.InputStream;
025import java.lang.reflect.Constructor;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Set;
029
030import org.apache.directory.api.ldap.model.entry.DefaultEntry;
031import org.apache.directory.api.ldap.model.exception.LdapException;
032import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
033import org.apache.directory.api.ldap.model.ldif.LdifEntry;
034import org.apache.directory.api.ldap.model.ldif.LdifReader;
035import org.apache.directory.api.ldap.model.name.Dn;
036import org.apache.directory.api.ldap.model.schema.SchemaManager;
037import org.apache.directory.api.util.Network;
038import org.apache.directory.api.util.Strings;
039import org.apache.directory.server.core.annotations.AnnotationUtils;
040import org.apache.directory.server.core.annotations.ApplyLdifFiles;
041import org.apache.directory.server.core.annotations.ApplyLdifs;
042import org.apache.directory.server.core.annotations.ContextEntry;
043import org.apache.directory.server.core.annotations.CreateAuthenticator;
044import org.apache.directory.server.core.annotations.CreateDS;
045import org.apache.directory.server.core.annotations.CreateIndex;
046import org.apache.directory.server.core.annotations.CreatePartition;
047import org.apache.directory.server.core.annotations.LoadSchema;
048import org.apache.directory.server.core.api.DirectoryService;
049import org.apache.directory.server.core.api.DnFactory;
050import org.apache.directory.server.core.api.interceptor.Interceptor;
051import org.apache.directory.server.core.api.partition.Partition;
052import org.apache.directory.server.core.authn.AuthenticationInterceptor;
053import org.apache.directory.server.core.authn.Authenticator;
054import org.apache.directory.server.core.authn.DelegatingAuthenticator;
055import org.apache.directory.server.core.partition.impl.btree.AbstractBTreePartition;
056import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex;
057import org.apache.directory.server.core.partition.impl.btree.mavibot.MavibotIndex;
058import org.apache.directory.server.i18n.I18n;
059import org.junit.runner.Description;
060import org.slf4j.Logger;
061import org.slf4j.LoggerFactory;
062
063
064/**
065 * A Helper class used to create a DS from the annotations
066 * 
067 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
068 */
069public final class DSAnnotationProcessor
070{
071    /** A logger for this class */
072    private static final Logger LOG = LoggerFactory.getLogger( DSAnnotationProcessor.class );
073
074
075    private DSAnnotationProcessor()
076    {
077    }
078
079
080    /**
081     * Create the DirectoryService
082     * 
083     * @param dsBuilder The DirectoryService builder
084     * @return an instance of DirectoryService
085     * @throws Exception If the DirectoryService cannot be created
086     */
087    public static DirectoryService createDS( CreateDS dsBuilder )
088        throws Exception
089    {
090        if ( LOG.isDebugEnabled() )
091        {
092            LOG.debug( "Starting DS {}...", dsBuilder.name() );
093        }
094        
095        Class<?> factory = dsBuilder.factory();
096        DirectoryServiceFactory dsf = ( DirectoryServiceFactory ) factory.newInstance();
097
098        DirectoryService service = dsf.getDirectoryService();
099        service.setAccessControlEnabled( dsBuilder.enableAccessControl() );
100        service.setAllowAnonymousAccess( dsBuilder.allowAnonAccess() );
101        service.getChangeLog().setEnabled( dsBuilder.enableChangeLog() );
102
103        dsf.init( dsBuilder.name() );
104
105        for ( Class<?> interceptorClass : dsBuilder.additionalInterceptors() )
106        {
107            service.addLast( ( Interceptor ) interceptorClass.newInstance() );
108        }
109
110        List<Interceptor> interceptorList = service.getInterceptors();
111
112        if ( dsBuilder.authenticators().length != 0 )
113        {
114            AuthenticationInterceptor authenticationInterceptor = null;
115
116            for ( Interceptor interceptor : interceptorList )
117            {
118                if ( interceptor instanceof AuthenticationInterceptor )
119                {
120                    authenticationInterceptor = ( AuthenticationInterceptor ) interceptor;
121                    break;
122                }
123            }
124
125            if ( authenticationInterceptor == null )
126            {
127                throw new IllegalStateException(
128                    "authentication interceptor not found" );
129            }
130
131            Set<Authenticator> authenticators = new HashSet<>();
132
133            for ( CreateAuthenticator createAuthenticator : dsBuilder
134                .authenticators() )
135            {
136                Authenticator auth = createAuthenticator.type().newInstance();
137
138                if ( auth instanceof DelegatingAuthenticator )
139                {
140                    DelegatingAuthenticator dauth = ( DelegatingAuthenticator ) auth;
141                    
142                    String host = createAuthenticator.delegateHost();
143                    
144                    if ( Strings.isEmpty( host ) )
145                    {
146                        host = Network.LOOPBACK_HOSTNAME;
147                    }
148                    
149                    dauth.setDelegateHost( host );
150                    dauth.setDelegatePort( createAuthenticator.delegatePort() );
151                    dauth.setDelegateSsl( createAuthenticator.delegateSsl() );
152                    dauth.setDelegateTls( createAuthenticator.delegateTls() );
153                    dauth.setBaseDn( service.getDnFactory().create( createAuthenticator.baseDn() ) );
154                    dauth.setDelegateSslTrustManagerFQCN( createAuthenticator.delegateSslTrustManagerFQCN() );
155                    dauth.setDelegateTlsTrustManagerFQCN( createAuthenticator.delegateTlsTrustManagerFQCN() );
156                }
157
158                authenticators.add( auth );
159            }
160
161            authenticationInterceptor.setAuthenticators( authenticators );
162            authenticationInterceptor.init( service );
163        }
164
165        service.setInterceptors( interceptorList );
166
167        SchemaManager schemaManager = service.getSchemaManager();
168
169        // process the schemas
170        for ( LoadSchema loadedSchema : dsBuilder.loadedSchemas() )
171        {
172            String schemaName = loadedSchema.name();
173            Boolean enabled = loadedSchema.enabled();
174
175            // Check if the schema is loaded or not
176            boolean isLoaded = schemaManager.isSchemaLoaded( schemaName );
177
178            if ( !isLoaded )
179            {
180                // We have to load the schema, if it exists
181                try
182                {
183                    isLoaded = schemaManager.load( schemaName );
184                }
185                catch ( LdapUnwillingToPerformException lutpe )
186                {
187                    // Cannot load the schema, it does not exists
188                    LOG.error( lutpe.getMessage() );
189                    continue;
190                }
191            }
192
193            if ( isLoaded )
194            {
195                if ( enabled )
196                {
197                    schemaManager.enable( schemaName );
198
199                    if ( schemaManager.isDisabled( schemaName ) )
200                    {
201                        LOG.error( "Cannot enable {}", schemaName );
202                    }
203                }
204                else
205                {
206                    schemaManager.disable( schemaName );
207
208                    if ( schemaManager.isEnabled( schemaName ) )
209                    {
210                        LOG.error( "Cannot disable {}", schemaName );
211                    }
212                }
213            }
214
215            LOG.debug( "Loading schema {}, enabled= {}", schemaName, enabled );
216        }
217
218        // Process the Partition, if any.
219        for ( CreatePartition createPartition : dsBuilder.partitions() )
220        {
221            Partition partition;
222
223            // Determine the partition type
224            if ( createPartition.type() == Partition.class )
225            {
226                // The annotation does not specify a specific partition type.
227                // We use the partition factory to create partition and index
228                // instances.
229                PartitionFactory partitionFactory = dsf.getPartitionFactory();
230                partition = partitionFactory.createPartition(
231                    schemaManager,
232                    service.getDnFactory(),
233                    createPartition.name(),
234                    createPartition.suffix(),
235                    createPartition.cacheSize(),
236                    new File( service.getInstanceLayout().getPartitionsDirectory(), createPartition.name() ) );
237
238                CreateIndex[] indexes = createPartition.indexes();
239
240                for ( CreateIndex createIndex : indexes )
241                {
242                    partitionFactory.addIndex( partition,
243                        createIndex.attribute(), createIndex.cacheSize() );
244                }
245
246                partition.initialize();
247            }
248            else
249            {
250                // The annotation contains a specific partition type, we use
251                // that type.
252                Class<?>[] partypes = new Class[]
253                    { SchemaManager.class, DnFactory.class };
254                Constructor<?> constructor = createPartition.type().getConstructor( partypes );
255                partition = ( Partition ) constructor.newInstance( schemaManager, service.getDnFactory() );
256                partition.setId( createPartition.name() );
257                partition.setSuffixDn( new Dn( schemaManager, createPartition.suffix() ) );
258
259                if ( partition instanceof AbstractBTreePartition )
260                {
261                    AbstractBTreePartition btreePartition = ( AbstractBTreePartition ) partition;
262                    btreePartition.setCacheSize( createPartition.cacheSize() );
263                    btreePartition.setPartitionPath( new File( service
264                        .getInstanceLayout().getPartitionsDirectory(),
265                        createPartition.name() ).toURI() );
266
267                    // Process the indexes if any
268                    CreateIndex[] indexes = createPartition.indexes();
269
270                    for ( CreateIndex createIndex : indexes )
271                    {
272                        if ( createIndex.type() == MavibotIndex.class )
273                        {
274                            // Mavibot index
275                            MavibotIndex index = new MavibotIndex( createIndex.attribute(), false );
276
277                            btreePartition.addIndexedAttributes( index );
278                        }
279                        else
280                        {
281                            // The annotation does not specify a specific index
282                            // type.
283                            // We use the generic index implementation.
284                            JdbmIndex index = new JdbmIndex( createIndex.attribute(), false );
285
286                            btreePartition.addIndexedAttributes( index );
287                        }
288                    }
289                }
290            }
291
292            partition.setSchemaManager( schemaManager );
293
294            // Inject the partition into the DirectoryService
295            service.addPartition( partition );
296
297            // Last, process the context entry
298            ContextEntry contextEntry = createPartition.contextEntry();
299
300            if ( contextEntry != null )
301            {
302                injectEntries( service, contextEntry.entryLdif() );
303            }
304        }
305
306        return service;
307    }
308
309
310    /**
311     * Create a DirectoryService from a Unit test annotation
312     * 
313     * @param description The annotations containing the info from which we will create
314     *  the DS
315     * @return A valid DirectoryService
316     * @throws Exception If the DirectoryService instance can't be returned
317     */
318    public static DirectoryService getDirectoryService( Description description )
319        throws Exception
320    {
321        CreateDS dsBuilder = description.getAnnotation( CreateDS.class );
322
323        if ( dsBuilder != null )
324        {
325            return createDS( dsBuilder );
326        }
327        else
328        {
329            LOG.debug( "No {} DS.", description.getDisplayName() );
330            return null;
331        }
332    }
333
334
335    /**
336     * Create a DirectoryService from an annotation. The @CreateDS annotation
337     * must be associated with either the method or the encapsulating class. We
338     * will first try to get the annotation from the method, and if there is
339     * none, then we try at the class level.
340     * 
341     * @return A valid DS
342     * @throws Exception If the DirectoryService instance can't be returned
343     */
344    public static DirectoryService getDirectoryService() throws Exception
345    {
346        Object instance = AnnotationUtils.getInstance( CreateDS.class );
347        CreateDS dsBuilder = null;
348
349        if ( instance != null )
350        {
351            dsBuilder = ( CreateDS ) instance;
352
353            // Ok, we have found a CreateDS annotation. Process it now.
354            return createDS( dsBuilder );
355        }
356
357        throw new LdapException( I18n.err( I18n.ERR_114 ) );
358    }
359
360
361    /**
362     * injects an LDIF entry in the given DirectoryService
363     * 
364     * @param entry the LdifEntry to be injected
365     * @param service the DirectoryService
366     * @throws Exception If the entry cannot be injected
367     */
368    private static void injectEntry( LdifEntry entry, DirectoryService service )
369        throws LdapException
370    {
371        if ( entry.isChangeAdd() || entry.isLdifContent() )
372        {
373            service.getAdminSession().add(
374                new DefaultEntry( service.getSchemaManager(), entry
375                    .getEntry() ) );
376        }
377        else if ( entry.isChangeModify() )
378        {
379            service.getAdminSession().modify( entry.getDn(),
380                entry.getModifications() );
381        }
382        else
383        {
384            String message = I18n.err( I18n.ERR_117, entry.getChangeType() );
385            throw new LdapException( message );
386        }
387    }
388
389
390    /**
391     * injects the LDIF entries present in a LDIF file
392     * 
393     * @param clazz The class which classLoaded will be use to retrieve the resources
394     * @param service the DirectoryService
395     * @param ldifFiles array of LDIF file names (only )
396     * @throws Exception If we weren't able to inject LdifFiles
397     */
398    public static void injectLdifFiles( Class<?> clazz,
399        DirectoryService service, String[] ldifFiles ) throws Exception
400    {
401        if ( ( ldifFiles != null ) && ( ldifFiles.length > 0 ) )
402        {
403            for ( String ldifFile : ldifFiles )
404            {
405                InputStream is = clazz.getClassLoader().getResourceAsStream(
406                    ldifFile );
407                if ( is == null )
408                {
409                    throw new FileNotFoundException( "LDIF file '" + ldifFile
410                        + "' not found." );
411                }
412                else
413                {
414                    LdifReader ldifReader = new LdifReader( is );
415
416                    for ( LdifEntry entry : ldifReader )
417                    {
418                        injectEntry( entry, service );
419                    }
420
421                    ldifReader.close();
422                }
423            }
424        }
425    }
426
427
428    /**
429     * Inject an ldif String into the server. Dn must be relative to the root.
430     * 
431     * @param service the directory service to use
432     * @param ldif the ldif containing entries to add to the server.
433     * @throws Exception if there is a problem adding the entries from the LDIF
434     */
435    public static void injectEntries( DirectoryService service, String ldif )
436        throws Exception
437    {
438        try ( LdifReader reader = new LdifReader() )
439        {
440            List<LdifEntry> entries = reader.parseLdif( ldif );
441    
442            for ( LdifEntry entry : entries )
443            {
444                injectEntry( entry, service );
445            }
446        }
447    }
448
449
450    /**
451     * Load the schemas, and enable/disable them.
452     * 
453     * @param desc The description
454     * @param service The DirectoryService instance
455     */
456    public static void loadSchemas( Description desc, DirectoryService service )
457    {
458        if ( desc == null )
459        {
460            return;
461        }
462
463        LoadSchema loadSchema = desc
464            .getAnnotation( LoadSchema.class );
465
466        if ( loadSchema != null )
467        {
468            System.out.println( loadSchema );
469        }
470    }
471
472
473    /**
474     * Apply the LDIF entries to the given service
475     * 
476     * @param desc The description
477     * @param service The DirectoryService instance
478     * @throws Exception If we can't apply the ldifs
479     */
480    public static void applyLdifs( Description desc, DirectoryService service )
481        throws Exception
482    {
483        if ( desc == null )
484        {
485            return;
486        }
487
488        ApplyLdifFiles applyLdifFiles = desc
489            .getAnnotation( ApplyLdifFiles.class );
490
491        if ( applyLdifFiles != null )
492        {
493            LOG.debug( "Applying {} to {}", applyLdifFiles.value(),
494                desc.getDisplayName() );
495            injectLdifFiles( applyLdifFiles.clazz(), service, applyLdifFiles.value() );
496        }
497
498        ApplyLdifs applyLdifs = desc.getAnnotation( ApplyLdifs.class );
499
500        if ( ( applyLdifs != null ) && ( applyLdifs.value() != null ) )
501        {
502            String[] ldifs = applyLdifs.value();
503
504            String dnStart = "dn:";
505
506            StringBuilder sb = new StringBuilder();
507
508            for ( int i = 0; i < ldifs.length; )
509            {
510                String s = ldifs[i++].trim();
511                if ( s.startsWith( dnStart ) )
512                {
513                    sb.append( s ).append( '\n' );
514
515                    // read the rest of lines till we encounter Dn again
516                    while ( i < ldifs.length )
517                    {
518                        s = ldifs[i++];
519                        if ( !s.startsWith( dnStart ) )
520                        {
521                            sb.append( s ).append( '\n' );
522                        }
523                        else
524                        {
525                            break;
526                        }
527                    }
528
529                    LOG.debug( "Applying {} to {}", sb, desc.getDisplayName() );
530                    injectEntries( service, sb.toString() );
531                    sb.setLength( 0 );
532
533                    i--; // step up a line
534                }
535            }
536        }
537    }
538}