View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.directory.server.config;
21  
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.Writer;
26  import java.lang.reflect.Field;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Set;
34  
35  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
36  import org.apache.directory.api.ldap.model.entry.Attribute;
37  import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
38  import org.apache.directory.api.ldap.model.exception.LdapException;
39  import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
40  import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
41  import org.apache.directory.api.ldap.model.ldif.LdifEntry;
42  import org.apache.directory.api.ldap.model.name.Dn;
43  import org.apache.directory.api.ldap.model.name.Rdn;
44  import org.apache.directory.api.ldap.model.schema.ObjectClass;
45  import org.apache.directory.api.ldap.model.schema.SchemaManager;
46  import org.apache.directory.api.util.Strings;
47  import org.apache.directory.server.config.beans.AdsBaseBean;
48  import org.apache.directory.server.config.beans.ConfigBean;
49  
50  
51  /**
52   * This class implements a writer for ApacheDS Configuration.
53   * <p>
54   * It can be used either:
55   * <ul>
56   *      <li>write the configuration to an LDIF</li>
57   *      <li>get the list of LDIF entries from the configuration</li>
58   * </ul>
59   * 
60   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
61   */
62  public class ConfigWriter
63  {
64      /** The schema manager */
65      private SchemaManager schemaManager;
66  
67      /** The configuration bean */
68      private ConfigBean configBean;
69  
70      /** The list of entries */
71      private List<LdifEntry> entries;
72  
73  
74      /**
75       * Creates a new instance of ConfigWriter.
76       *
77       * @param schemaManager
78       *      the schema manager
79       * @param configBean
80       *      the configuration bean
81       */
82      public ConfigWriter( SchemaManager schemaManager, ConfigBean configBean )
83      {
84          this.schemaManager = schemaManager;
85          this.configBean = configBean;
86      }
87  
88  
89      /**
90       * Converts the configuration bean to a list of LDIF entries.
91       */
92      private void convertConfigurationBeanToLdifEntries() throws ConfigurationException
93      {
94          try
95          {
96              if ( entries == null )
97              {
98                  entries = new ArrayList<>();
99  
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 }