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