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.config;
021
022
023import java.io.File;
024import java.io.FileWriter;
025import java.io.IOException;
026import java.lang.reflect.Field;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Set;
032
033import org.apache.directory.api.ldap.model.constants.SchemaConstants;
034import org.apache.directory.api.ldap.model.entry.Attribute;
035import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
036import org.apache.directory.api.ldap.model.exception.LdapException;
037import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
038import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
039import org.apache.directory.api.ldap.model.ldif.LdifEntry;
040import org.apache.directory.api.ldap.model.name.Dn;
041import org.apache.directory.api.ldap.model.name.Rdn;
042import org.apache.directory.api.ldap.model.schema.ObjectClass;
043import org.apache.directory.api.ldap.model.schema.SchemaManager;
044import org.apache.directory.api.util.Strings;
045import org.apache.directory.server.config.beans.AdsBaseBean;
046import org.apache.directory.server.config.beans.ConfigBean;
047
048
049/**
050 * This class implements a writer for ApacheDS Configuration.
051 * <p>
052 * It can be used either:
053 * <ul>
054 *      <li>write the configuration to an LDIF</li>
055 *      <li>get the list of LDIF entries from the configuration</li>
056 * </ul>
057 * 
058 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
059 */
060public class ConfigWriter
061{
062    /** The schema manager */
063    private SchemaManager schemaManager;
064
065    /** The configuration bean */
066    private ConfigBean configBean;
067
068    /** The list of entries */
069    private List<LdifEntry> entries;
070
071
072    /**
073     * Creates a new instance of ConfigWriter.
074     *
075     * @param schemaManager
076     *      the schema manager
077     * @param configBean
078     *      the configuration bean
079     */
080    public ConfigWriter( SchemaManager schemaManager, ConfigBean configBean )
081    {
082        this.schemaManager = schemaManager;
083        this.configBean = configBean;
084    }
085
086
087    /**
088     * Converts the configuration bean to a list of LDIF entries.
089     */
090    private void convertConfigurationBeanToLdifEntries() throws ConfigurationException
091    {
092        try
093        {
094            if ( entries == null )
095            {
096                entries = new ArrayList<LdifEntry>();
097
098                // Building the default config root entry 'ou=config'
099                LdifEntry configRootEntry = new LdifEntry();
100                configRootEntry.setDn( new Dn( SchemaConstants.OU_AT + "=" + "config" ) );
101                addObjectClassAttribute( schemaManager, configRootEntry, "organizationalUnit" );
102                addAttributeTypeValues( SchemaConstants.OU_AT, "config", configRootEntry );
103                entries.add( configRootEntry );
104
105                // Building entries from the directory service beans
106                List<AdsBaseBean> directoryServiceBeans = configBean.getDirectoryServiceBeans();
107                for ( AdsBaseBean adsBaseBean : directoryServiceBeans )
108                {
109                    addBean( configRootEntry.getDn(), schemaManager, adsBaseBean, entries );
110                }
111            }
112        }
113        catch ( Exception e )
114        {
115            throw new ConfigurationException( "Unable to convert the configuration bean to LDIF entries", e );
116        }
117    }
118
119
120    /**
121     * Writes the configuration bean as LDIF to the given file.
122     *
123     * @param path
124     *      the output file path
125     * @throws ConfigurationException
126     *      if an error occurs during the conversion to LDIF
127     * @throws IOException
128     *      if an error occurs when writing the file
129     */
130    public void writeToPath( String path ) throws ConfigurationException, IOException
131    {
132        writeToFile( new File( path ) );
133    }
134
135
136    /**
137     * Writes the configuration bean as LDIF to the given file.
138     *
139     * @param file
140     *      the output file
141     * @throws ConfigurationException
142     *      if an error occurs during the conversion to LDIF
143     * @throws IOException
144     *      if an error occurs when writing the file
145     */
146    public void writeToFile( File file ) throws ConfigurationException, IOException
147    {
148        // Writing the file to disk
149        FileWriter writer = new FileWriter( file );
150        writer.append( writeToString() );
151        writer.close();
152    }
153
154
155    /**
156     * Writes the configuration to a String object.
157     *
158     * @return
159     *      a String containing the LDIF 
160     *      representation of the configuration
161     * @throws ConfigurationException
162     *      if an error occurs during the conversion to LDIF
163     */
164    public String writeToString() throws ConfigurationException
165    {
166        // Converting the configuration bean to a list of LDIF entries
167        convertConfigurationBeanToLdifEntries();
168
169        // Building the StringBuilder
170        StringBuilder sb = new StringBuilder();
171        sb.append( "version: 1\n" );
172        for ( LdifEntry entry : entries )
173        {
174            sb.append( entry.toString() );
175        }
176
177        return sb.toString();
178    }
179
180
181    /**
182     * Gets the converted LDIF entries from the configuration bean.
183     *
184     * @return
185     *      the list of converted LDIF entries
186     * @throws ConfigurationException
187     *      if an error occurs during the conversion to LDIF
188     */
189    public List<LdifEntry> getConvertedLdifEntries() throws ConfigurationException
190    {
191        // Converting the configuration bean to a list of LDIF entries
192        convertConfigurationBeanToLdifEntries();
193
194        // Returning the list of entries
195        return entries;
196    }
197
198
199    /**
200     * Adds the computed 'objectClass' attribute for the given entry and object class name.
201     *
202     * @param schemaManager
203     *      the schema manager
204     * @param entry
205     *      the entry
206     * @param objectClass
207     *      the object class name
208     * @throws LdapException
209     */
210    private void addObjectClassAttribute( SchemaManager schemaManager, LdifEntry entry, String objectClass )
211        throws LdapException
212    {
213        ObjectClass objectClassObject = schemaManager.lookupObjectClassRegistry( objectClass );
214        if ( objectClassObject != null )
215        {
216            // Building the list of 'objectClass' attribute values
217            Set<String> objectClassAttributeValues = new HashSet<String>();
218            computeObjectClassAttributeValues( schemaManager, objectClassAttributeValues, objectClassObject );
219
220            // Adding values to the entry
221            addAttributeTypeValues( SchemaConstants.OBJECT_CLASS_AT, objectClassAttributeValues, entry );
222        }
223        else
224        {
225            throw new IllegalStateException( "Missing object class " + objectClass );
226        }
227    }
228
229
230    /**
231     * Recursively computes the 'objectClass' attribute values set.
232     *
233     * @param schemaManager
234     *      the schema manager
235     * @param objectClassAttributeValues
236     *      the set containing the values
237     * @param objectClass
238     *      the current object class
239     * @throws LdapException
240     */
241    private void computeObjectClassAttributeValues( SchemaManager schemaManager,
242        Set<String> objectClassAttributeValues,
243        ObjectClass objectClass ) throws LdapException
244    {
245        ObjectClass topObjectClass = schemaManager.lookupObjectClassRegistry( SchemaConstants.TOP_OC );
246        if ( topObjectClass == null )
247        {
248            throw new IllegalStateException( "Missing top object class." );
249        }
250
251        if ( topObjectClass.equals( objectClass ) )
252        {
253            objectClassAttributeValues.add( objectClass.getName() );
254        }
255        else
256        {
257            objectClassAttributeValues.add( objectClass.getName() );
258
259            List<ObjectClass> superiors = objectClass.getSuperiors();
260            if ( ( superiors != null ) && ( superiors.size() > 0 ) )
261            {
262                for ( ObjectClass superior : superiors )
263                {
264                    computeObjectClassAttributeValues( schemaManager, objectClassAttributeValues, superior );
265                }
266            }
267            else
268            {
269                objectClassAttributeValues.add( topObjectClass.getName() );
270            }
271        }
272    }
273
274
275    /**
276     * Adds a configuration bean to the list of entries.
277     *
278     * @param rootDn
279     *      the current root Dn
280     * @param schemaManager
281     *      the schema manager
282     * @param bean
283     *      the configuration bean
284     * @param entries
285     *      the list of the entries
286     * @throws Exception
287     */
288    private void addBean( Dn rootDn, SchemaManager schemaManager, AdsBaseBean bean, List<LdifEntry> entries )
289        throws Exception
290    {
291        addBean( rootDn, schemaManager, bean, entries, null, null );
292    }
293
294
295    /**
296     * Adds a configuration bean to the list of entries.
297     *
298     * @param rootDn
299     *      the current root Dn
300     * @param schemaManager
301     *      the schema manager
302     * @param bean
303     *      the configuration bean
304     * @param entries
305     *      the list of the entries
306     * @param parentEntry
307     *      the parent entry
308     * @param attributeTypeForParentEntry
309     *      the attribute type to use when adding the value of 
310     *      the Rdn to the parent entry
311     * @throws Exception
312     */
313    private void addBean( Dn rootDn, SchemaManager schemaManager, AdsBaseBean bean, List<LdifEntry> entries,
314        LdifEntry parentEntry, String attributeTypeForParentEntry )
315        throws Exception
316    {
317        if ( bean != null )
318        {
319            // Getting the class of the bean
320            Class<?> beanClass = bean.getClass();
321
322            // Creating the entry to hold the bean and adding it to the list
323            LdifEntry entry = new LdifEntry();
324            entry.setDn( getDn( rootDn, bean ) );
325            addObjectClassAttribute( schemaManager, entry, getObjectClassNameForBean( beanClass ) );
326            entries.add( entry );
327
328            // A flag to know when we reached the 'AdsBaseBean' class when 
329            // looping on the class hierarchy of the bean
330            boolean adsBaseBeanClassFound = false;
331
332            // Looping until the 'AdsBaseBean' class has been found
333            while ( !adsBaseBeanClassFound )
334            {
335                // Checking if we reached the 'AdsBaseBean' class
336                if ( beanClass == AdsBaseBean.class )
337                {
338                    adsBaseBeanClassFound = true;
339                }
340
341                // Looping on all fields of the bean
342                Field[] fields = beanClass.getDeclaredFields();
343                for ( Field field : fields )
344                {
345                    // Making the field accessible (we get an exception if we don't do that)
346                    field.setAccessible( true );
347
348                    // Getting the class of the field
349                    Class<?> fieldClass = field.getType();
350                    Object fieldValue = field.get( bean );
351
352                    // Looking for the @ConfigurationElement annotation
353                    ConfigurationElement configurationElement = field.getAnnotation( ConfigurationElement.class );
354                    if ( configurationElement != null )
355                    {
356                        // Getting the annotation's values
357                        String attributeType = configurationElement.attributeType();
358                        String objectClass = configurationElement.objectClass();
359                        String container = configurationElement.container();
360                        boolean isOptional = configurationElement.isOptional();
361                        String defaultValue = configurationElement.defaultValue();
362
363                        // Checking if we have a value for the attribute type
364                        if ( ( attributeType != null ) && ( !"".equals( attributeType ) ) )
365                        {
366                            // Checking if the field is optional and if the default value matches
367                            if ( isOptional )
368                            {
369                                if ( ( defaultValue != null ) && ( fieldValue != null )
370                                    && ( defaultValue.equalsIgnoreCase( fieldValue.toString() ) ) )
371                                {
372                                    // Skipping the addition of the value
373                                    continue;
374                                }
375                            }
376
377                            // Adding values to the entry
378                            addAttributeTypeValues( configurationElement.attributeType(), fieldValue, entry );
379
380                            continue;
381                        }
382                        // Checking if we have a value for the object class
383                        else if ( ( objectClass != null ) && ( !"".equals( objectClass ) ) )
384                        {
385                            // Checking if we're dealing with a container
386                            if ( ( container != null ) && ( !"".equals( container ) ) )
387                            {
388                                // Creating the entry for the container and adding it to the list
389                                LdifEntry containerEntry = new LdifEntry();
390                                containerEntry.setDn( entry.getDn().add( new Rdn( SchemaConstants.OU_AT, container ) ) );
391                                addObjectClassAttribute( schemaManager, containerEntry,
392                                    SchemaConstants.ORGANIZATIONAL_UNIT_OC );
393                                addAttributeTypeValues( SchemaConstants.OU_AT, container, containerEntry );
394                                entries.add( containerEntry );
395
396                                if ( Collection.class.isAssignableFrom( fieldClass ) )
397                                {
398                                    // Looping on the Collection's objects
399                                    @SuppressWarnings("unchecked")
400                                    Collection<Object> collection = ( Collection<Object> ) fieldValue;
401                                    if ( collection != null )
402                                    {
403                                        for ( Object object : collection )
404                                        {
405                                            if ( object instanceof AdsBaseBean )
406                                            {
407                                                // Adding the bean
408                                                addBean( containerEntry.getDn(), schemaManager, ( AdsBaseBean ) object,
409                                                    entries, entry, attributeType );
410
411                                                continue;
412                                            }
413                                            else
414                                            {
415                                                // TODO throw an error, if we have a container, the type must be a subtype of AdsBaseBean
416                                                throw new Exception();
417                                            }
418                                        }
419                                    }
420                                }
421                                else
422                                {
423                                    // TODO throw an error, if we have a container, the type must be a subtype of Collection
424                                    throw new Exception();
425                                }
426                            }
427                            else
428                            {
429                                // Adding the bean
430                                addBean( entry.getDn(), schemaManager, ( AdsBaseBean ) fieldValue, entries, entry,
431                                    attributeType );
432                            }
433                        }
434                    }
435                }
436
437                // Moving to the upper class in the class hierarchy
438                beanClass = beanClass.getSuperclass();
439            }
440        }
441    }
442
443
444    /**
445     * Gets the name of the object class to use for the given bean class.
446     *
447     * @param c
448     *      the bean class
449     * @return
450     *      the name of the object class to use for the given bean class
451     */
452    private String getObjectClassNameForBean( Class<?> c )
453    {
454        String classNameWithPackage = getClassNameWithoutPackageName( c );
455        return "ads-" + classNameWithPackage.substring( 0, classNameWithPackage.length() - 4 );
456    }
457
458
459    /**
460     * Gets the class name of the given class stripped from its package name.
461     *
462     * @param c
463     *      the class
464     * @return
465     *      the class name of the given class stripped from its package name
466     */
467    private String getClassNameWithoutPackageName( Class<?> c )
468    {
469        String className = c.getName();
470
471        int firstChar = className.lastIndexOf( '.' ) + 1;
472        if ( firstChar > 0 )
473        {
474            return className.substring( firstChar );
475        }
476
477        return className;
478    }
479
480
481    /**
482     * Indicates the given type is multiple.
483     *
484     * @param clazz
485     *      the class
486     * @return
487     *      <code>true</code> if the given is multiple,
488     *      <code>false</code> if not.
489     */
490    private boolean isMultiple( Class<?> clazz )
491    {
492        return Collection.class.isAssignableFrom( clazz );
493    }
494
495
496    /**
497     * Gets the Dn associated with the configuration bean based on the given base Dn.
498     *
499     * @param baseDn
500     *      the base Dn
501     * @param bean
502     *      the configuration bean
503     * @return
504     *      the Dn associated with the configuration bean based on the given base Dn.
505     * @throws LdapInvalidDnException
506     * @throws IllegalArgumentException
507     * @throws IllegalAccessException
508     * @throws LdapInvalidAttributeValueException 
509     */
510    private Dn getDn( Dn baseDn, AdsBaseBean bean ) throws LdapInvalidDnException, IllegalArgumentException,
511        IllegalAccessException, LdapInvalidAttributeValueException
512    {
513        // Getting the class of the bean
514        Class<?> beanClass = bean.getClass();
515
516        // A flag to know when we reached the 'AdsBaseBean' class when 
517        // looping on the class hierarchy of the bean
518        boolean adsBaseBeanClassFound = false;
519
520        // Looping until the 'AdsBaseBean' class has been found
521        while ( !adsBaseBeanClassFound )
522        {
523            // Checking if we reached the 'AdsBaseBean' class
524            if ( beanClass == AdsBaseBean.class )
525            {
526                adsBaseBeanClassFound = true;
527            }
528
529            // Looping on all fields of the bean
530            Field[] fields = beanClass.getDeclaredFields();
531            for ( Field field : fields )
532            {
533                // Making the field accessible (we get an exception if we don't do that)
534                field.setAccessible( true );
535
536                // Looking for the @ConfigurationElement annotation and
537                // if the field is the Rdn
538                ConfigurationElement configurationElement = field.getAnnotation( ConfigurationElement.class );
539                if ( ( configurationElement != null ) && ( configurationElement.isRdn() ) )
540                {
541                    return baseDn.add( new Rdn( configurationElement.attributeType(), field.get( bean ).toString() ) );
542                }
543            }
544
545            // Moving to the upper class in the class hierarchy
546            beanClass = beanClass.getSuperclass();
547        }
548
549        return Dn.EMPTY_DN; // TODO Throw an error when we reach that point
550    }
551
552
553    /**
554     * Adds values for an attribute type to the given entry.
555     *
556     * @param attributeType
557     *      the attribute type
558     * @param value
559     *      the value
560     * @param entry
561     *      the entry
562     * @throws org.apache.directory.api.ldap.model.exception.LdapException
563     */
564    private void addAttributeTypeValues( String attributeType, Object o, LdifEntry entry )
565        throws LdapException
566    {
567        // We don't store a 'null' value
568        if ( o != null )
569        {
570            // Is the value multiple?
571            if ( isMultiple( o.getClass() ) )
572            {
573                // Adding each single value separately
574                Collection<?> values = ( Collection<?> ) o;
575                if ( values != null )
576                {
577                    for ( Object value : values )
578                    {
579                        addAttributeTypeValue( attributeType, value, entry );
580                    }
581                }
582            }
583            else
584            {
585                // Adding the single value
586                addAttributeTypeValue( attributeType, o, entry );
587            }
588        }
589    }
590
591
592    /**
593     * Adds a value, either byte[] or another type (converted into a String 
594     * via the Object.toString() method), to the attribute.
595     *
596     * @param attributeType
597     *      the attribute type
598     * @param value
599     *      the value
600     * @param entry
601     *      the entry
602     */
603    private void addAttributeTypeValue( String attributeType, Object value, LdifEntry entry ) throws LdapException
604    {
605        // We don't store a 'null' value
606        if ( value != null )
607        {
608            // Getting the attribute from the entry
609            Attribute attribute = entry.get( attributeType );
610
611            // If no attribute has been found, we need to create it and add it to the entry
612            if ( attribute == null )
613            {
614                attribute = new DefaultAttribute( attributeType );
615                entry.addAttribute( attribute );
616            }
617
618            // Storing the value to the attribute
619            if ( value instanceof byte[] )
620            {
621                // Value is a byte[]
622                attribute.add( ( byte[] ) value );
623            }
624            // Storing the boolean value in UPPERCASE (TRUE or FALSE) to the attribute
625            else if ( value instanceof Boolean )
626            {
627                // Value is a byte[]
628                attribute.add( Strings.toUpperCase( value.toString() ) );
629            }
630            else
631            {
632                // Value is another type of object that we store as a String
633                // (There will be an automatic translation for primary types like int, long, etc.)
634                attribute.add( value.toString() );
635            }
636        }
637    }
638}