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}