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.api.ldap.model.ldif; 021 022 023import java.io.BufferedReader; 024import java.io.Closeable; 025import java.io.DataInputStream; 026import java.io.File; 027import java.io.FileNotFoundException; 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.Reader; 032import java.io.StringReader; 033import java.net.MalformedURLException; 034import java.net.URL; 035import java.nio.charset.Charset; 036import java.nio.file.Files; 037import java.nio.file.Paths; 038import java.util.ArrayList; 039import java.util.Iterator; 040import java.util.List; 041import java.util.NoSuchElementException; 042 043import org.apache.directory.api.asn1.util.Oid; 044import org.apache.directory.api.i18n.I18n; 045import org.apache.directory.api.ldap.model.constants.SchemaConstants; 046import org.apache.directory.api.ldap.model.entry.Attribute; 047import org.apache.directory.api.ldap.model.entry.DefaultAttribute; 048import org.apache.directory.api.ldap.model.entry.ModificationOperation; 049import org.apache.directory.api.ldap.model.exception.LdapException; 050import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException; 051import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; 052import org.apache.directory.api.ldap.model.message.Control; 053import org.apache.directory.api.ldap.model.name.Ava; 054import org.apache.directory.api.ldap.model.name.Dn; 055import org.apache.directory.api.ldap.model.name.Rdn; 056import org.apache.directory.api.ldap.model.schema.AttributeType; 057import org.apache.directory.api.ldap.model.schema.SchemaManager; 058import org.apache.directory.api.util.Base64; 059import org.apache.directory.api.util.Chars; 060import org.apache.directory.api.util.Strings; 061import org.apache.directory.api.util.exception.NotImplementedException; 062import org.slf4j.Logger; 063import org.slf4j.LoggerFactory; 064 065 066/** 067 * <pre> 068 * <ldif-file> ::= "version:" <fill> <number> <seps> <dn-spec> <sep> 069 * <ldif-content-change> 070 * 071 * <ldif-content-change> ::= 072 * <number> <oid> <options-e> <value-spec> <sep> 073 * <attrval-specs-e> <ldif-attrval-record-e> | 074 * <alpha> <chars-e> <options-e> <value-spec> <sep> 075 * <attrval-specs-e> <ldif-attrval-record-e> | 076 * "control:" <fill> <number> <oid> <spaces-e> 077 * <criticality> <value-spec-e> <sep> <controls-e> 078 * "changetype:" <fill> <changerecord-type> <ldif-change-record-e> | 079 * "changetype:" <fill> <changerecord-type> <ldif-change-record-e> 080 * 081 * <ldif-attrval-record-e> ::= <seps> <dn-spec> <sep> <attributeType> 082 * <options-e> <value-spec> <sep> <attrval-specs-e> 083 * <ldif-attrval-record-e> | e 084 * 085 * <ldif-change-record-e> ::= <seps> <dn-spec> <sep> <controls-e> 086 * "changetype:" <fill> <changerecord-type> <ldif-change-record-e> | e 087 * 088 * <dn-spec> ::= "dn:" <fill> <safe-string> | "dn::" <fill> <base64-string> 089 * 090 * <controls-e> ::= "control:" <fill> <number> <oid> <spaces-e> <criticality> 091 * <value-spec-e> <sep> <controls-e> | e 092 * 093 * <criticality> ::= "true" | "false" | e 094 * 095 * <oid> ::= '.' <number> <oid> | e 096 * 097 * <attrval-specs-e> ::= <number> <oid> <options-e> <value-spec> 098 * <sep> <attrval-specs-e> | 099 * <alpha> <chars-e> <options-e> <value-spec> <sep> <attrval-specs-e> | e 100 * 101 * <value-spec-e> ::= <value-spec> | e 102 * 103 * <value-spec> ::= ':' <fill> <safe-string-e> | 104 * "::" <fill> <base64-chars> | 105 * ":<" <fill> <url> 106 * 107 * <attributeType> ::= <number> <oid> | <alpha> <chars-e> 108 * 109 * <options-e> ::= ';' <char> <chars-e> <options-e> |e 110 * 111 * <chars-e> ::= <char> <chars-e> | e 112 * 113 * <changerecord-type> ::= "add" <sep> <attributeType> 114 * <options-e> <value-spec> <sep> <attrval-specs-e> | 115 * "delete" <sep> | 116 * "modify" <sep> <mod-type> <fill> <attributeType> 117 * <options-e> <sep> <attrval-specs-e> <sep> '-' <sep> <mod-specs-e> | 118 * "moddn" <sep> <newrdn> <sep> "deleteoldrdn:" 119 * <fill> <0-1> <sep> <newsuperior-e> <sep> | 120 * "modrdn" <sep> <newrdn> <sep> "deleteoldrdn:" 121 * <fill> <0-1> <sep> <newsuperior-e> <sep> 122 * 123 * <newrdn> ::= ':' <fill> <safe-string> | "::" <fill> <base64-chars> 124 * 125 * <newsuperior-e> ::= "newsuperior" <newrdn> | e 126 * 127 * <mod-specs-e> ::= <mod-type> <fill> <attributeType> <options-e> 128 * <sep> <attrval-specs-e> <sep> '-' <sep> <mod-specs-e> | e 129 * 130 * <mod-type> ::= "add:" | "delete:" | "replace:" 131 * 132 * <url> ::= <a Uniform Resource Locator, as defined in [6]> 133 * 134 * 135 * 136 * LEXICAL 137 * ------- 138 * 139 * <fill> ::= ' ' <fill> | e 140 * <char> ::= <alpha> | <digit> | '-' 141 * <number> ::= <digit> <digits> 142 * <0-1> ::= '0' | '1' 143 * <digits> ::= <digit> <digits> | e 144 * <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 145 * <seps> ::= <sep> <seps-e> 146 * <seps-e> ::= <sep> <seps-e> | e 147 * <sep> ::= 0x0D 0x0A | 0x0A 148 * <spaces> ::= ' ' <spaces-e> 149 * <spaces-e> ::= ' ' <spaces-e> | e 150 * <safe-string-e> ::= <safe-string> | e 151 * <safe-string> ::= <safe-init-char> <safe-chars> 152 * <safe-init-char> ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x1F] | [0x21-0x39] | 0x3B | [0x3D-0x7F] 153 * <safe-chars> ::= <safe-char> <safe-chars> | e 154 * <safe-char> ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x7F] 155 * <base64-string> ::= <base64-char> <base64-chars> 156 * <base64-chars> ::= <base64-char> <base64-chars> | e 157 * <base64-char> ::= 0x2B | 0x2F | [0x30-0x39] | 0x3D | [0x41-9x5A] | [0x61-0x7A] 158 * <alpha> ::= [0x41-0x5A] | [0x61-0x7A] 159 * 160 * COMMENTS 161 * -------- 162 * - The ldap-oid VN is not correct in the RFC-2849. It has been changed from 1*DIGIT 0*1("." 1*DIGIT) to 163 * DIGIT+ ("." DIGIT+)* 164 * - The mod-spec lacks a sep between *attrval-spec and "-". 165 * - The BASE64-UTF8-STRING should be BASE64-CHAR BASE64-STRING 166 * - The ValueSpec rule must accept multilines values. In this case, we have a LF followed by a 167 * single space before the continued value. 168 * </pre> 169 * The relaxed mode is used when a SchemaManager is injected. 170 * 171 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 172 */ 173public class LdifReader implements Iterable<LdifEntry>, Closeable 174{ 175 /** A logger */ 176 private static final Logger LOG = LoggerFactory.getLogger( LdifReader.class ); 177 178 /** A list of read lines */ 179 protected List<String> lines; 180 181 /** The current position */ 182 protected int position; 183 184 /** The ldif file version default value */ 185 protected static final int DEFAULT_VERSION = 1; 186 187 /** The ldif version */ 188 protected int version; 189 190 /** Type of element read : ENTRY */ 191 protected static final int LDIF_ENTRY = 0; 192 193 /** Type of element read : CHANGE */ 194 protected static final int CHANGE = 1; 195 196 /** Type of element read : UNKNOWN */ 197 protected static final int UNKNOWN = 2; 198 199 /** Size limit for file contained values */ 200 protected long sizeLimit = SIZE_LIMIT_DEFAULT; 201 202 /** The default size limit : 1Mo */ 203 protected static final long SIZE_LIMIT_DEFAULT = 1024000; 204 205 /** State values for the modify operation : MOD_SPEC */ 206 protected static final int MOD_SPEC = 0; 207 208 /** State values for the modify operation : ATTRVAL_SPEC */ 209 protected static final int ATTRVAL_SPEC = 1; 210 211 /** State values for the modify operation : ATTRVAL_SPEC_OR_SEP */ 212 protected static final int ATTRVAL_SPEC_OR_SEP = 2; 213 214 /** Iterator prefetched entry */ 215 protected LdifEntry prefetched; 216 217 /** The ldif Reader */ 218 protected Reader reader; 219 220 /** A flag set if the ldif contains entries */ 221 protected boolean containsEntries; 222 223 /** A flag set if the ldif contains changes */ 224 protected boolean containsChanges; 225 226 /** The SchemaManager instance, if any */ 227 protected SchemaManager schemaManager; 228 229 /** 230 * An Exception to handle error message, has Iterator.next() can't throw 231 * exceptions 232 */ 233 protected Exception error; 234 235 /** total length of an LDIF entry including the comments */ 236 protected int entryLen = 0; 237 238 /** the parsed entry's starting position */ 239 protected long entryOffset = 0; 240 241 /** the current offset of the reader */ 242 protected long offset = 0; 243 244 /** the numer of the current line being parsed by the reader */ 245 protected int lineNumber; 246 247 /** flag to turn on/off of the DN validation. By default DNs are validated after parsing */ 248 protected boolean validateDn = true; 249 250 /** A counter used to create facked OIDs */ 251 private int oidCounter = 0; 252 253 254 /** 255 * Constructors 256 */ 257 public LdifReader() 258 { 259 lines = new ArrayList<>(); 260 position = 0; 261 version = DEFAULT_VERSION; 262 } 263 264 265 /** 266 * Creates a Schema aware reader 267 * 268 * @param schemaManager The SchemaManager 269 */ 270 public LdifReader( SchemaManager schemaManager ) 271 { 272 lines = new ArrayList<>(); 273 position = 0; 274 version = DEFAULT_VERSION; 275 this.schemaManager = schemaManager; 276 } 277 278 279 /** 280 * A constructor which takes a file name. Default charset is used. 281 * 282 * @param ldifFileName A file name containing ldif formated input 283 * @throws LdapLdifException If the file cannot be processed or if the format is incorrect 284 */ 285 public LdifReader( String ldifFileName ) throws LdapLdifException 286 { 287 this( new File( ldifFileName ) ); 288 } 289 290 291 /** 292 * A constructor which takes a Reader. 293 * 294 * @param in A Reader containing ldif formated input 295 * @throws LdapException If the file cannot be processed or if the format is incorrect 296 */ 297 public LdifReader( Reader in ) throws LdapException 298 { 299 initReader( new BufferedReader( in ) ); 300 } 301 302 303 /** 304 * A constructor which takes an InputStream. Default charset is used. 305 * 306 * @param in An InputStream containing ldif formated input 307 * @throws LdapException If the file cannot be processed or if the format is incorrect 308 */ 309 public LdifReader( InputStream in ) throws LdapException 310 { 311 initReader( new BufferedReader( new InputStreamReader( in, Charset.defaultCharset() ) ) ); 312 } 313 314 315 /** 316 * A constructor which takes a File. Default charset is used. 317 * 318 * @param file A File containing ldif formated input 319 * @throws LdapLdifException If the file cannot be processed or if the format is incorrect 320 */ 321 public LdifReader( File file ) throws LdapLdifException 322 { 323 this( file, null ); 324 } 325 326 327 /** 328 * A constructor which takes a File and a SchemaManager. Default charset is used. 329 * 330 * @param file A File containing ldif formated input 331 * @param schemaManager The SchemaManager instance to use 332 * @throws LdapLdifException If the file cannot be processed or if the format is incorrect 333 */ 334 public LdifReader( File file, SchemaManager schemaManager ) throws LdapLdifException 335 { 336 if ( !file.exists() ) 337 { 338 String msg = I18n.err( I18n.ERR_13443_CANNOT_FIND_FILE, file.getAbsoluteFile() ); 339 LOG.error( msg ); 340 throw new LdapLdifException( msg ); 341 } 342 343 if ( !file.canRead() ) 344 { 345 String msg = I18n.err( I18n.ERR_13444_CANNOT_READ_FILE, file.getName() ); 346 LOG.error( msg ); 347 throw new LdapLdifException( msg ); 348 } 349 350 this.schemaManager = schemaManager; 351 352 try 353 { 354 InputStream is = Files.newInputStream( Paths.get( file.getPath() ) ); 355 initReader( 356 new BufferedReader( new InputStreamReader( is, Charset.defaultCharset() ) ) ); 357 } 358 catch ( FileNotFoundException fnfe ) 359 { 360 String msg = I18n.err( I18n.ERR_13443_CANNOT_FIND_FILE, file.getAbsoluteFile() ); 361 LOG.error( msg ); 362 throw new LdapLdifException( msg, fnfe ); 363 } 364 catch ( LdapInvalidDnException lide ) 365 { 366 throw new LdapLdifException( lide.getMessage(), lide ); 367 } 368 catch ( IOException ioe ) 369 { 370 throw new LdapLdifException( ioe.getMessage(), ioe ); 371 } 372 catch ( LdapException le ) 373 { 374 throw new LdapLdifException( le.getMessage(), le ); 375 } 376 } 377 378 379 /** 380 * Store the reader and initialize the LdifReader 381 * 382 * @param reader The reader to use 383 * @throws LdapException If the initialization failed 384 */ 385 private void initReader( BufferedReader reader ) throws LdapException 386 { 387 this.reader = reader; 388 init(); 389 } 390 391 392 /** 393 * Initialize the LdifReader 394 * 395 * @throws LdapException If the initialization failed 396 */ 397 public void init() throws LdapException 398 { 399 lines = new ArrayList<>(); 400 position = 0; 401 version = DEFAULT_VERSION; 402 containsChanges = false; 403 containsEntries = false; 404 405 // First get the version - if any - 406 version = parseVersion(); 407 prefetched = parseEntry(); 408 } 409 410 411 /** 412 * @return The ldif file version 413 */ 414 public int getVersion() 415 { 416 return version; 417 } 418 419 420 /** 421 * @return The maximum size of a file which is used into an attribute value. 422 */ 423 public long getSizeLimit() 424 { 425 return sizeLimit; 426 } 427 428 429 /** 430 * Set the maximum file size that can be accepted for an attribute value 431 * 432 * @param sizeLimit The size in bytes 433 */ 434 public void setSizeLimit( long sizeLimit ) 435 { 436 this.sizeLimit = sizeLimit; 437 } 438 439 440 // <fill> ::= ' ' <fill> | e 441 private void parseFill( char[] document ) 442 { 443 while ( Chars.isCharASCII( document, position, ' ' ) ) 444 { 445 position++; 446 } 447 } 448 449 450 /** 451 * Parse a number following the rules : 452 * 453 * <number> ::= <digit> <digits> <digits> ::= <digit> <digits> | e <digit> 454 * ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 455 * 456 * Check that the number is in the interval 457 * 458 * @param document The document containing the number to parse 459 * @return a String representing the parsed number 460 */ 461 private String parseNumber( char[] document ) 462 { 463 int initPos = position; 464 465 while ( true ) 466 { 467 if ( Chars.isDigit( document, position ) ) 468 { 469 position++; 470 } 471 else 472 { 473 break; 474 } 475 } 476 477 if ( position == initPos ) 478 { 479 return null; 480 } 481 else 482 { 483 return new String( document, initPos, position - initPos ); 484 } 485 } 486 487 488 /** 489 * Parse the changeType 490 * 491 * @param line The line which contains the changeType 492 * @return The operation. 493 */ 494 protected ChangeType parseChangeType( String line ) 495 { 496 ChangeType operation = ChangeType.Add; 497 498 String modOp = Strings.trim( line.substring( "changetype:".length() ) ); 499 500 if ( "add".equalsIgnoreCase( modOp ) ) 501 { 502 operation = ChangeType.Add; 503 } 504 else if ( "delete".equalsIgnoreCase( modOp ) ) 505 { 506 operation = ChangeType.Delete; 507 } 508 else if ( "modify".equalsIgnoreCase( modOp ) ) 509 { 510 operation = ChangeType.Modify; 511 } 512 else if ( "moddn".equalsIgnoreCase( modOp ) ) 513 { 514 operation = ChangeType.ModDn; 515 } 516 else if ( "modrdn".equalsIgnoreCase( modOp ) ) 517 { 518 operation = ChangeType.ModRdn; 519 } 520 521 return operation; 522 } 523 524 525 /** 526 * Parse the Dn of an entry 527 * 528 * @param line The line to parse 529 * @return A Dn 530 * @throws LdapLdifException If the Dn is invalid 531 */ 532 protected String parseDn( String line ) throws LdapLdifException 533 { 534 String dn; 535 536 String lowerLine = Strings.toLowerCaseAscii( line ); 537 538 if ( lowerLine.startsWith( "dn:" ) || lowerLine.startsWith( "Dn:" ) ) 539 { 540 // Ok, we have a Dn. Is it base 64 encoded ? 541 int length = line.length(); 542 543 if ( length == 3 ) 544 { 545 // The Dn is empty : it's a rootDSE 546 dn = ""; 547 } 548 else if ( line.charAt( 3 ) == ':' ) 549 { 550 if ( length > 4 ) 551 { 552 // This is a base 64 encoded Dn. 553 String trimmedLine = line.substring( 4 ).trim(); 554 555 dn = Strings.utf8ToString( Base64.decode( trimmedLine.toCharArray() ) ); 556 } 557 else 558 { 559 // The Dn is empty : error 560 LOG.error( I18n.err( I18n.ERR_13404_EMPTY_DN_NOT_ALLOWED, lineNumber ) ); 561 throw new LdapLdifException( I18n.err( I18n.ERR_13445_NO_DN ) ); 562 } 563 } 564 else 565 { 566 dn = line.substring( 3 ).trim(); 567 } 568 } 569 else 570 { 571 LOG.error( I18n.err( I18n.ERR_13405_DN_EXPECTED, lineNumber ) ); 572 throw new LdapLdifException( I18n.err( I18n.ERR_13445_NO_DN ) ); 573 } 574 575 // Check that the Dn is valid. If not, an exception will be thrown 576 if ( validateDn && !Dn.isValid( dn ) ) 577 { 578 String message = I18n.err( I18n.ERR_13446_INVALID_DN, dn, lineNumber ); 579 LOG.error( message ); 580 throw new LdapLdifException( message ); 581 } 582 583 return dn; 584 } 585 586 587 /** 588 * Parse the value part. 589 * 590 * @param line The line which contains the value 591 * @param pos The starting position in the line 592 * @return A String or a byte[], depending of the kind of value we get 593 */ 594 protected static Object parseSimpleValue( String line, int pos ) 595 { 596 if ( line.length() > pos + 1 ) 597 { 598 char c = line.charAt( pos + 1 ); 599 600 if ( c == ':' ) 601 { 602 String value = Strings.trim( line.substring( pos + 2 ) ); 603 604 return Base64.decode( value.toCharArray() ); 605 } 606 else 607 { 608 return Strings.trim( line.substring( pos + 1 ) ); 609 } 610 } 611 else 612 { 613 return null; 614 } 615 } 616 617 618 private Object getValue( String attributeName, byte[] value ) 619 { 620 if ( schemaManager != null ) 621 { 622 AttributeType attributeType = schemaManager.getAttributeType( attributeName ); 623 624 if ( attributeType != null ) 625 { 626 if ( attributeType.getSyntax().isHumanReadable() ) 627 { 628 return Strings.utf8ToString( value ); 629 } 630 else 631 { 632 return value; 633 } 634 } 635 else 636 { 637 return value; 638 } 639 } 640 else 641 { 642 return value; 643 } 644 } 645 646 647 /** 648 * Parse the value part. 649 * 650 * @param attributeName The attribute name 651 * @param line The line which contains the value 652 * @param pos The starting position in the line 653 * @return A String or a byte[], depending of the kind of value we get 654 * @throws LdapLdifException If something went wrong 655 */ 656 protected Object parseValue( String attributeName, String line, int pos ) throws LdapLdifException 657 { 658 if ( line.length() > pos + 1 ) 659 { 660 char c = line.charAt( pos + 1 ); 661 662 if ( c == ':' ) 663 { 664 String value = Strings.trim( line.substring( pos + 2 ) ); 665 666 byte[] decoded = Base64.decode( value.toCharArray() ); 667 668 return getValue( attributeName, decoded ); 669 } 670 else if ( c == '<' ) 671 { 672 String urlName = Strings.trim( line.substring( pos + 2 ) ); 673 674 try 675 { 676 URL url = new URL( urlName ); 677 678 if ( "file".equals( url.getProtocol() ) ) 679 { 680 String fileName = url.getFile(); 681 682 File file = new File( fileName ); 683 684 if ( !file.exists() ) 685 { 686 LOG.error( I18n.err( I18n.ERR_13406_FILE_NOT_FOUND, fileName, lineNumber ) ); 687 throw new LdapLdifException( I18n.err( I18n.ERR_13447_BAD_URL_FILE_NOT_FOUND ) ); 688 } 689 else 690 { 691 long length = file.length(); 692 693 if ( length > sizeLimit ) 694 { 695 String message = I18n.err( I18n.ERR_13448_FILE_TOO_BIG, fileName, lineNumber ); 696 LOG.error( message ); 697 throw new LdapLdifException( message ); 698 } 699 else 700 { 701 byte[] data = new byte[( int ) length]; 702 703 try ( DataInputStream inf = new DataInputStream( 704 Files.newInputStream( Paths.get( fileName ) ) ) ) 705 { 706 inf.readFully( data ); 707 708 return getValue( attributeName, data ); 709 } 710 catch ( FileNotFoundException fnfe ) 711 { 712 // We can't reach this point, the file 713 // existence has already been 714 // checked 715 LOG.error( I18n.err( I18n.ERR_13406_FILE_NOT_FOUND, fileName, lineNumber ) ); 716 throw new LdapLdifException( I18n.err( I18n.ERR_13447_BAD_URL_FILE_NOT_FOUND ), 717 fnfe ); 718 } 719 catch ( IOException ioe ) 720 { 721 LOG.error( I18n.err( I18n.ERR_13407_ERROR_READING_FILE, fileName, lineNumber ) ); 722 throw new LdapLdifException( I18n.err( I18n.ERR_13449_ERROR_READING_BAD_URL ), ioe ); 723 } 724 } 725 } 726 } 727 else 728 { 729 LOG.error( I18n.err( I18n.ERR_13408_BAD_PROTOCOL ) ); 730 throw new LdapLdifException( I18n.err( I18n.ERR_13451_UNSUPPORTED_PROTOCOL, lineNumber ) ); 731 } 732 } 733 catch ( MalformedURLException mue ) 734 { 735 String message = I18n.err( I18n.ERR_13452_BAD_URL, urlName, lineNumber ); 736 LOG.error( message ); 737 throw new LdapLdifException( message, mue ); 738 } 739 } 740 else 741 { 742 String value = Strings.trimLeft( line.substring( pos + 1 ) ); 743 int end = value.length(); 744 745 for ( int i = value.length() - 1; i > 0; i-- ) 746 { 747 char cc = value.charAt( i ); 748 749 if ( cc == ' ' ) 750 { 751 if ( value.charAt( i - 1 ) == '\\' ) 752 { 753 // Escaped space : do nothing 754 break; 755 } 756 else 757 { 758 end = i; 759 } 760 } 761 else 762 { 763 break; 764 } 765 } 766 767 String result = null; 768 769 if ( end > 0 ) 770 { 771 result = value.substring( 0, end ); 772 } 773 774 return result; 775 } 776 } 777 else 778 { 779 return null; 780 } 781 } 782 783 784 /** 785 * Parse a control. The grammar is : 786 * <pre> 787 * <control> ::= "control:" <fill> <ldap-oid> <critical-e> <value-spec-e> <sep> 788 * <critical-e> ::= <spaces> <boolean> | e 789 * <boolean> ::= "true" | "false" 790 * <value-spec-e> ::= <value-spec> | e 791 * <value-spec> ::= ":" <fill> <SAFE-STRING-e> | "::" <fill> <BASE64-STRING> | ":<" <fill> <url> 792 * </pre> 793 * 794 * It can be read as : 795 * <pre> 796 * "control:" <fill> <ldap-oid> [ " "+ ( "true" | 797 * "false") ] [ ":" <fill> <SAFE-STRING-e> | "::" <fill> <BASE64-STRING> | ":<" 798 * <fill> <url> ] 799 * </pre> 800 * 801 * @param line The line containing the control 802 * @return A control 803 * @exception LdapLdifException If the control has no OID or if the OID is incorrect, 804 * of if the criticality is not set when it's mandatory. 805 */ 806 private Control parseControl( String line ) throws LdapLdifException 807 { 808 String lowerLine = Strings.toLowerCaseAscii( line ).trim(); 809 char[] controlValue = line.trim().toCharArray(); 810 int pos = 0; 811 int length = controlValue.length; 812 813 // Get the <ldap-oid> 814 if ( pos > length ) 815 { 816 // No OID : error ! 817 String msg = I18n.err( I18n.ERR_13409_CONTROL_WITHOUT_OID, lineNumber ); 818 LOG.error( msg ); 819 throw new LdapLdifException( msg ); 820 } 821 822 int initPos = pos; 823 824 while ( Chars.isCharASCII( controlValue, pos, '.' ) || Chars.isDigit( controlValue, pos ) ) 825 { 826 pos++; 827 } 828 829 if ( pos == initPos ) 830 { 831 // Not a valid OID ! 832 String msg = I18n.err( I18n.ERR_13409_CONTROL_WITHOUT_OID, lineNumber ); 833 LOG.error( msg ); 834 throw new LdapLdifException( msg ); 835 } 836 837 // Create and check the OID 838 String oidString = lowerLine.substring( 0, pos ); 839 840 if ( !Oid.isOid( oidString ) ) 841 { 842 String message = I18n.err( I18n.ERR_13453_INVALID_OID, oidString, lineNumber ); 843 LOG.error( message ); 844 throw new LdapLdifException( message ); 845 } 846 847 LdifControl control = new LdifControl( oidString ); 848 849 // Get the criticality, if any 850 // Skip the <fill> 851 while ( Chars.isCharASCII( controlValue, pos, ' ' ) ) 852 { 853 pos++; 854 } 855 856 // Check if we have a "true" or a "false" 857 int criticalPos = lowerLine.indexOf( ':' ); 858 859 int criticalLength; 860 861 if ( criticalPos == -1 ) 862 { 863 criticalLength = length - pos; 864 } 865 else 866 { 867 criticalLength = criticalPos - pos; 868 } 869 870 if ( ( criticalLength == 4 ) && ( "true".equalsIgnoreCase( lowerLine.substring( pos, pos + 4 ) ) ) ) 871 { 872 control.setCritical( true ); 873 } 874 else if ( ( criticalLength == 5 ) && ( "false".equalsIgnoreCase( lowerLine.substring( pos, pos + 5 ) ) ) ) 875 { 876 control.setCritical( false ); 877 } 878 else if ( criticalLength != 0 ) 879 { 880 // If we have a criticality, it should be either "true" or "false", 881 // nothing else 882 String msg = I18n.err( I18n.ERR_13410_INVALID_CRITICALITY, lineNumber ); 883 LOG.error( msg ); 884 throw new LdapLdifException( msg ); 885 } 886 887 if ( criticalPos > 0 ) 888 { 889 // We have a value. It can be a normal value, a base64 encoded value 890 // or a file contained value 891 if ( Chars.isCharASCII( controlValue, criticalPos + 1, ':' ) ) 892 { 893 // Base 64 encoded value 894 895 // Skip the <fill> 896 pos = criticalPos + 2; 897 898 while ( Chars.isCharASCII( controlValue, pos, ' ' ) ) 899 { 900 pos++; 901 } 902 903 byte[] value = Base64.decode( line.substring( pos ).toCharArray() ); 904 control.setValue( value ); 905 } 906 else if ( Chars.isCharASCII( controlValue, criticalPos + 1, '<' ) ) 907 { 908 // File contained value 909 throw new NotImplementedException( I18n.err( I18n.ERR_13433_SEE_DIRSERVER_1547 ) ); 910 } 911 else 912 { 913 // Skip the <fill> 914 pos = criticalPos + 1; 915 916 while ( Chars.isCharASCII( controlValue, pos, ' ' ) ) 917 { 918 pos++; 919 } 920 921 // Standard value 922 byte[] value = new byte[length - pos]; 923 924 for ( int i = 0; i < length - pos; i++ ) 925 { 926 value[i] = ( byte ) controlValue[i + pos]; 927 } 928 929 control.setValue( value ); 930 } 931 } 932 933 return control; 934 } 935 936 937 /** 938 * Parse an AttributeType/AttributeValue 939 * 940 * @param line The line to parse 941 * @return the parsed Attribute 942 */ 943 public static Attribute parseAttributeValue( String line ) 944 { 945 int colonIndex = line.indexOf( ':' ); 946 947 if ( colonIndex != -1 ) 948 { 949 String attributeType = line.substring( 0, colonIndex ); 950 Object attributeValue = parseSimpleValue( line, colonIndex ); 951 952 // Create an attribute 953 if ( attributeValue instanceof String ) 954 { 955 return new DefaultAttribute( attributeType, ( String ) attributeValue ); 956 } 957 else 958 { 959 return new DefaultAttribute( attributeType, ( byte[] ) attributeValue ); 960 } 961 } 962 else 963 { 964 return null; 965 } 966 } 967 968 969 /** 970 * Parse an AttributeType/AttributeValue 971 * 972 * @param entry The entry where to store the value 973 * @param line The line to parse 974 * @param lowerLine The same line, lowercased 975 * @throws LdapException If anything goes wrong 976 */ 977 public void parseAttributeValue( LdifEntry entry, String line, String lowerLine ) throws LdapException 978 { 979 int colonIndex = line.indexOf( ':' ); 980 981 String attributeType = lowerLine.substring( 0, colonIndex ); 982 983 // We should *not* have a Dn twice 984 if ( "dn".equals( attributeType ) ) 985 { 986 LOG.error( I18n.err( I18n.ERR_13400_ENTRY_WITH_TWO_DNS, lineNumber ) ); 987 throw new LdapLdifException( I18n.err( I18n.ERR_13439_LDIF_ENTRY_WITH_TWO_DNS ) ); 988 } 989 990 Object attributeValue = parseValue( attributeType, line, colonIndex ); 991 992 if ( schemaManager != null ) 993 { 994 AttributeType at = schemaManager.getAttributeType( attributeType ); 995 996 if ( at != null ) 997 { 998 if ( at.getSyntax().isHumanReadable() ) 999 { 1000 if ( attributeValue == null ) 1001 { 1002 attributeValue = ""; 1003 } 1004 else if ( attributeValue instanceof byte[] ) 1005 { 1006 attributeValue = Strings.utf8ToString( ( byte[] ) attributeValue ); 1007 } 1008 } 1009 else 1010 { 1011 if ( attributeValue instanceof String ) 1012 { 1013 attributeValue = Strings.getBytesUtf8( ( String ) attributeValue ); 1014 } 1015 } 1016 } 1017 } 1018 1019 // Update the entry 1020 try 1021 { 1022 entry.addAttribute( attributeType, attributeValue ); 1023 } 1024 catch ( Exception e ) 1025 { 1026 // The attribute does not exist already, create a fake one 1027 if ( ( schemaManager != null ) && schemaManager.isRelaxed() ) 1028 { 1029 AttributeType newAttributeType = new AttributeType( "1.3.6.1.4.1.18060.0.9999." + oidCounter++ ); 1030 newAttributeType.setNames( attributeType ); 1031 newAttributeType.setSyntax( schemaManager.getLdapSyntaxRegistry().get( SchemaConstants.DIRECTORY_STRING_SYNTAX ) ); 1032 schemaManager.add( newAttributeType ); 1033 entry.addAttribute( attributeType, attributeValue ); 1034 } 1035 } 1036 } 1037 1038 1039 /** 1040 * Parse a ModRDN operation 1041 * 1042 * @param entry The entry to update 1043 * @param iter The lines iterator 1044 * @throws LdapLdifException If anything goes wrong 1045 */ 1046 private void parseModRdn( LdifEntry entry, Iterator<String> iter ) throws LdapLdifException 1047 { 1048 // We must have two lines : one starting with "newrdn:" or "newrdn::", 1049 // and the second starting with "deleteoldrdn:" 1050 if ( iter.hasNext() ) 1051 { 1052 String line = iter.next(); 1053 String lowerLine = Strings.toLowerCaseAscii( line ); 1054 1055 if ( lowerLine.startsWith( "newrdn::" ) || lowerLine.startsWith( "newrdn:" ) ) 1056 { 1057 int colonIndex = line.indexOf( ':' ); 1058 Object attributeValue = parseValue( null, line, colonIndex ); 1059 1060 if ( attributeValue instanceof String ) 1061 { 1062 entry.setNewRdn( ( String ) attributeValue ); 1063 } 1064 else 1065 { 1066 entry.setNewRdn( Strings.utf8ToString( ( byte[] ) attributeValue ) ); 1067 } 1068 } 1069 else 1070 { 1071 String msg = I18n.err( I18n.ERR_13411_BAD_MODRDN_OPERATION, lineNumber ); 1072 LOG.error( msg ); 1073 throw new LdapLdifException( msg ); 1074 } 1075 } 1076 else 1077 { 1078 String msg = I18n.err( I18n.ERR_13411_BAD_MODRDN_OPERATION, lineNumber ); 1079 LOG.error( msg ); 1080 throw new LdapLdifException( msg ); 1081 } 1082 1083 if ( iter.hasNext() ) 1084 { 1085 String line = iter.next(); 1086 String lowerLine = Strings.toLowerCaseAscii( line ); 1087 1088 if ( lowerLine.startsWith( "deleteoldrdn:" ) ) 1089 { 1090 int colonIndex = line.indexOf( ':' ); 1091 Object attributeValue = parseValue( null, line, colonIndex ); 1092 entry.setDeleteOldRdn( "1".equals( attributeValue ) ); 1093 } 1094 else 1095 { 1096 String msg = I18n.err( I18n.ERR_13412_NO_DELETEOLDRDN, lineNumber ); 1097 LOG.error( msg ); 1098 throw new LdapLdifException( msg ); 1099 } 1100 } 1101 else 1102 { 1103 String msg = I18n.err( I18n.ERR_13412_NO_DELETEOLDRDN, lineNumber ); 1104 LOG.error( msg ); 1105 throw new LdapLdifException( msg ); 1106 } 1107 } 1108 1109 1110 /** 1111 * Parse a modify change type. 1112 * 1113 * The grammar is : 1114 * <pre> 1115 * <changerecord> ::= "changetype:" FILL "modify" SEP <mod-spec> <mod-specs-e> 1116 * <mod-spec> ::= "add:" <mod-val> | "delete:" <mod-val-del> | "replace:" <mod-val> 1117 * | "increment:" <mod-val> 1118 * <mod-specs-e> ::= <mod-spec> 1119 * <mod-specs-e> | e 1120 * <mod-val> ::= FILL ATTRIBUTE-DESCRIPTION SEP ATTRVAL-SPEC <attrval-specs-e> "-" SEP 1121 * <mod-val-del> ::= FILL ATTRIBUTE-DESCRIPTION SEP <attrval-specs-e> "-" SEP 1122 * <attrval-specs-e> ::= ATTRVAL-SPEC <attrval-specs> | e 1123 * </pre> 1124 * 1125 * @param entry The entry to feed 1126 * @param iter The lines 1127 * @exception LdapLdifException If the modify operation is invalid 1128 */ 1129 private void parseModify( LdifEntry entry, Iterator<String> iter ) throws LdapLdifException 1130 { 1131 int state = MOD_SPEC; 1132 String modified = null; 1133 ModificationOperation modificationType = ModificationOperation.ADD_ATTRIBUTE; 1134 Attribute attribute = null; 1135 1136 // The following flag is used to deal with empty modifications 1137 boolean isEmptyValue = true; 1138 1139 while ( iter.hasNext() ) 1140 { 1141 String line = iter.next(); 1142 String lowerLine = Strings.toLowerCaseAscii( line ); 1143 1144 if ( lowerLine.startsWith( "-" ) ) 1145 { 1146 if ( ( state != ATTRVAL_SPEC_OR_SEP ) && ( state != ATTRVAL_SPEC ) ) 1147 { 1148 String msg = I18n.err( I18n.ERR_13413_BAD_MODIFY_SEPARATOR, lineNumber ); 1149 LOG.error( msg ); 1150 throw new LdapLdifException( msg ); 1151 } 1152 else 1153 { 1154 if ( isEmptyValue ) 1155 { 1156 if ( state == ATTRVAL_SPEC_OR_SEP ) 1157 { 1158 entry.addModification( modificationType, modified ); 1159 } 1160 else 1161 { 1162 // Update the entry with a null value 1163 entry.addModification( modificationType, modified, null ); 1164 } 1165 } 1166 else 1167 { 1168 // Update the entry with the attribute 1169 entry.addModification( modificationType, attribute ); 1170 } 1171 1172 state = MOD_SPEC; 1173 isEmptyValue = true; 1174 } 1175 } 1176 else if ( lowerLine.startsWith( "add:" ) ) 1177 { 1178 if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) 1179 { 1180 String msg = I18n.err( I18n.ERR_13414_BAD_MODIFY_SEPARATOR_2, lineNumber ); 1181 LOG.error( msg ); 1182 throw new LdapLdifException( msg ); 1183 } 1184 1185 modified = Strings.trim( line.substring( "add:".length() ) ); 1186 modificationType = ModificationOperation.ADD_ATTRIBUTE; 1187 attribute = new DefaultAttribute( modified ); 1188 1189 state = ATTRVAL_SPEC; 1190 } 1191 else if ( lowerLine.startsWith( "delete:" ) ) 1192 { 1193 if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) 1194 { 1195 String msg = I18n.err( I18n.ERR_13414_BAD_MODIFY_SEPARATOR_2, lineNumber ); 1196 LOG.error( msg ); 1197 throw new LdapLdifException( msg ); 1198 } 1199 1200 modified = Strings.trim( line.substring( "delete:".length() ) ); 1201 modificationType = ModificationOperation.REMOVE_ATTRIBUTE; 1202 attribute = new DefaultAttribute( modified ); 1203 isEmptyValue = false; 1204 1205 state = ATTRVAL_SPEC_OR_SEP; 1206 } 1207 else if ( lowerLine.startsWith( "replace:" ) ) 1208 { 1209 if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) 1210 { 1211 String msg = I18n.err( I18n.ERR_13414_BAD_MODIFY_SEPARATOR_2, lineNumber ); 1212 LOG.error( msg ); 1213 throw new LdapLdifException( msg ); 1214 } 1215 1216 modified = Strings.trim( line.substring( "replace:".length() ) ); 1217 modificationType = ModificationOperation.REPLACE_ATTRIBUTE; 1218 1219 if ( schemaManager != null ) 1220 { 1221 AttributeType attributeType = schemaManager.getAttributeType( modified ); 1222 attribute = new DefaultAttribute( modified, attributeType ); 1223 } 1224 else 1225 { 1226 attribute = new DefaultAttribute( modified ); 1227 } 1228 1229 state = ATTRVAL_SPEC_OR_SEP; 1230 } 1231 else if ( lowerLine.startsWith( "increment:" ) ) 1232 { 1233 if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) 1234 { 1235 String msg = I18n.err( I18n.ERR_13414_BAD_MODIFY_SEPARATOR_2, lineNumber ); 1236 LOG.error( msg ); 1237 throw new LdapLdifException( msg ); 1238 } 1239 1240 modified = Strings.trim( line.substring( "increment:".length() ) ); 1241 modificationType = ModificationOperation.INCREMENT_ATTRIBUTE; 1242 1243 if ( schemaManager != null ) 1244 { 1245 AttributeType attributeType = schemaManager.getAttributeType( modified ); 1246 attribute = new DefaultAttribute( modified, attributeType ); 1247 } 1248 else 1249 { 1250 attribute = new DefaultAttribute( modified ); 1251 } 1252 1253 state = ATTRVAL_SPEC_OR_SEP; 1254 } 1255 else 1256 { 1257 if ( ( state != ATTRVAL_SPEC ) && ( state != ATTRVAL_SPEC_OR_SEP ) ) 1258 { 1259 String msg = I18n.err( I18n.ERR_13413_BAD_MODIFY_SEPARATOR, lineNumber ); 1260 LOG.error( msg ); 1261 throw new LdapLdifException( msg ); 1262 } 1263 1264 // A standard AttributeType/AttributeValue pair 1265 int colonIndex = line.indexOf( ':' ); 1266 1267 String attributeType = line.substring( 0, colonIndex ); 1268 1269 if ( !attributeType.equalsIgnoreCase( modified ) ) 1270 { 1271 LOG.error( I18n.err( I18n.ERR_13415_MOD_ATTR_AND_VALUE_SPEC_NOT_EQUAL, lineNumber ) ); 1272 throw new LdapLdifException( I18n.err( I18n.ERR_13454_BAD_MODIFY_ATTRIBUTE ) ); 1273 } 1274 1275 // We should *not* have a Dn twice 1276 if ( "dn".equalsIgnoreCase( attributeType ) ) 1277 { 1278 LOG.error( I18n.err( I18n.ERR_13400_ENTRY_WITH_TWO_DNS, lineNumber ) ); 1279 throw new LdapLdifException( I18n.err( I18n.ERR_13439_LDIF_ENTRY_WITH_TWO_DNS ) ); 1280 } 1281 1282 Object attributeValue = parseValue( attributeType, line, colonIndex ); 1283 1284 try 1285 { 1286 if ( attributeValue instanceof String ) 1287 { 1288 attribute.add( ( String ) attributeValue ); 1289 } 1290 else 1291 { 1292 attribute.add( ( byte[] ) attributeValue ); 1293 } 1294 } 1295 catch ( LdapInvalidAttributeValueException liave ) 1296 { 1297 throw new LdapLdifException( liave.getMessage(), liave ); 1298 } 1299 1300 isEmptyValue = false; 1301 1302 state = ATTRVAL_SPEC_OR_SEP; 1303 } 1304 } 1305 1306 if ( state != MOD_SPEC ) 1307 { 1308 String msg = I18n.err( I18n.ERR_13414_BAD_MODIFY_SEPARATOR_2, lineNumber ); 1309 LOG.error( msg ); 1310 throw new LdapLdifException( msg ); 1311 } 1312 } 1313 1314 1315 /** 1316 * Parse a change operation. We have to handle different cases depending on 1317 * the operation. 1318 * <ul> 1319 * <li>1) Delete : there should *not* be any line after the "changetype: delete" </li> 1320 * <li>2) Add : we must have a list of AttributeType : AttributeValue elements </li> 1321 * <li>3) ModDN : we must have two following lines: a "newrdn:" and a "deleteoldrdn:" </li> 1322 * <li>4) ModRDN : the very same, but a "newsuperior:" line is expected </li> 1323 * <li>5) Modify</li> 1324 * </ul> 1325 * 1326 * The grammar is : 1327 * <pre> 1328 * <changerecord> ::= "changetype:" FILL "add" SEP <attrval-spec> <attrval-specs-e> | 1329 * "changetype:" FILL "delete" | 1330 * "changetype:" FILL "modrdn" SEP <newrdn> SEP <deleteoldrdn> SEP | 1331 * // To be checked 1332 * "changetype:" FILL "moddn" SEP <newrdn> SEP <deleteoldrdn> SEP <newsuperior> SEP | 1333 * "changetype:" FILL "modify" SEP <mod-spec> <mod-specs-e> 1334 * <newrdn> ::= "newrdn:" FILL Rdn | "newrdn::" FILL BASE64-Rdn 1335 * <deleteoldrdn> ::= "deleteoldrdn:" FILL "0" | "deleteoldrdn:" FILL "1" 1336 * <newsuperior> ::= "newsuperior:" FILL Dn | "newsuperior::" FILL BASE64-Dn 1337 * <mod-specs-e> ::= <mod-spec> <mod-specs-e> | e 1338 * <mod-spec> ::= "add:" <mod-val> | "delete:" <mod-val> | "replace:" <mod-val> 1339 * <mod-val> ::= FILL ATTRIBUTE-DESCRIPTION SEP ATTRVAL-SPEC <attrval-specs-e> "-" SEP 1340 * <attrval-specs-e> ::= ATTRVAL-SPEC <attrval-specs> | e 1341 * </pre> 1342 * 1343 * @param entry The entry to feed 1344 * @param iter The lines iterator 1345 * @param operation The change operation (add, modify, delete, moddn or modrdn) 1346 * @exception LdapException If the change operation is invalid 1347 */ 1348 private void parseChange( LdifEntry entry, Iterator<String> iter, ChangeType operation ) throws LdapException 1349 { 1350 // The changetype and operation has already been parsed. 1351 entry.setChangeType( operation ); 1352 1353 switch ( operation ) 1354 { 1355 case Delete: 1356 // The change type will tell that it's a delete operation, 1357 // the dn is used as a key. 1358 return; 1359 1360 case Add: 1361 // We will iterate through all attribute/value pairs 1362 while ( iter.hasNext() ) 1363 { 1364 String line = iter.next(); 1365 String lowerLine = Strings.toLowerCaseAscii( line ); 1366 parseAttributeValue( entry, line, lowerLine ); 1367 } 1368 1369 return; 1370 1371 case Modify: 1372 parseModify( entry, iter ); 1373 return; 1374 1375 case ModDn: 1376 // They are supposed to have the same syntax : 1377 // No break ! 1378 case ModRdn: 1379 // First, parse the modrdn part 1380 parseModRdn( entry, iter ); 1381 1382 // The next line should be the new superior, if we have one 1383 if ( iter.hasNext() ) 1384 { 1385 String line = iter.next(); 1386 String lowerLine = Strings.toLowerCaseAscii( line ); 1387 1388 if ( lowerLine.startsWith( "newsuperior:" ) ) 1389 { 1390 int colonIndex = line.indexOf( ':' ); 1391 Object attributeValue = parseValue( null, line, colonIndex ); 1392 1393 if ( attributeValue instanceof String ) 1394 { 1395 entry.setNewSuperior( ( String ) attributeValue ); 1396 } 1397 else 1398 { 1399 entry.setNewSuperior( Strings.utf8ToString( ( byte[] ) attributeValue ) ); 1400 } 1401 } 1402 else 1403 { 1404 if ( operation == ChangeType.ModDn ) 1405 { 1406 LOG.error( I18n.err( I18n.ERR_13416_NEW_SUPERIOR_NEEDED, lineNumber ) ); 1407 throw new LdapLdifException( I18n.err( I18n.ERR_13455_BAD_MODDN_NO_SUPERIOR ) ); 1408 } 1409 } 1410 } 1411 1412 return; 1413 1414 default: 1415 // This is an error 1416 LOG.error( I18n.err( I18n.ERR_13417_UNKNOWN_OPERATION, lineNumber ) ); 1417 throw new LdapLdifException( I18n.err( I18n.ERR_13456_BAD_OPERATION ) ); 1418 } 1419 } 1420 1421 1422 /** 1423 * Parse a ldif file. The following rules are processed : 1424 * <pre> 1425 * <ldif-file> ::= <ldif-attrval-record> <ldif-attrval-records> | 1426 * <ldif-change-record> <ldif-change-records> 1427 * <ldif-attrval-record> ::= <dn-spec> <sep> <attrval-spec> <attrval-specs> 1428 * <ldif-change-record> ::= <dn-spec> <sep> <controls-e> <changerecord> 1429 * <dn-spec> ::= "dn:" <fill> <distinguishedName> | "dn::" <fill> <base64-distinguishedName> 1430 * <changerecord> ::= "changetype:" <fill> <change-op> 1431 * </pre> 1432 * 1433 * @return the parsed ldifEntry 1434 * @exception LdapException If the ldif file does not contain a valid entry 1435 */ 1436 protected LdifEntry parseEntry() throws LdapException 1437 { 1438 if ( ( lines == null ) || lines.isEmpty() ) 1439 { 1440 if ( LOG.isDebugEnabled() ) 1441 { 1442 LOG.debug( I18n.msg( I18n.MSG_13408_END_OF_LDIF ) ); 1443 } 1444 1445 return null; 1446 } 1447 1448 // The entry must start with a dn: or a dn:: 1449 String line = lines.get( 0 ); 1450 1451 lineNumber -= ( lines.size() - 1 ); 1452 1453 String name = parseDn( line ); 1454 1455 Dn dn = null; 1456 1457 try 1458 { 1459 dn = new Dn( schemaManager, name ); 1460 } 1461 catch ( LdapInvalidDnException lide ) 1462 { 1463 // Deal with the RDN whihc is not in the schema 1464 // First parse the DN without the schema 1465 dn = new Dn( name ); 1466 1467 Rdn rdn = dn.getRdn(); 1468 1469 // Process each Ava 1470 for ( Ava ava : rdn ) 1471 { 1472 if ( ( schemaManager != null ) && ( schemaManager.getAttributeType( ava.getType() ) == null ) 1473 && schemaManager.isRelaxed() ) 1474 { 1475 // Not found : create a new one 1476 AttributeType newAttributeType = new AttributeType( "1.3.6.1.4.1.18060.0.9999." + oidCounter++ ); 1477 newAttributeType.setNames( ava.getType() ); 1478 newAttributeType.setSyntax( schemaManager.getLdapSyntaxRegistry().get( SchemaConstants.DIRECTORY_STRING_SYNTAX ) ); 1479 schemaManager.add( newAttributeType ); 1480 } 1481 } 1482 1483 dn = new Dn( schemaManager, name ); 1484 } 1485 1486 // Ok, we have found a Dn 1487 LdifEntry entry = createLdifEntry( schemaManager ); 1488 entry.setLengthBeforeParsing( entryLen ); 1489 entry.setOffset( entryOffset ); 1490 1491 entry.setDn( dn ); 1492 1493 // We remove this dn from the lines 1494 lines.remove( 0 ); 1495 1496 // Now, let's iterate through the other lines 1497 Iterator<String> iter = lines.iterator(); 1498 1499 // This flag is used to distinguish between an entry and a change 1500 int type = LDIF_ENTRY; 1501 1502 // The following boolean is used to check that a control is *not* 1503 // found elswhere than just after the dn 1504 boolean controlSeen = false; 1505 1506 // We use this boolean to check that we do not have AttributeValues 1507 // after a change operation 1508 boolean changeTypeSeen = false; 1509 1510 ChangeType operation = ChangeType.Add; 1511 String lowerLine; 1512 Control control; 1513 1514 while ( iter.hasNext() ) 1515 { 1516 lineNumber++; 1517 1518 // Each line could start either with an OID, an attribute type, with 1519 // "control:" or with "changetype:" 1520 line = iter.next(); 1521 lowerLine = Strings.toLowerCaseAscii( line ); 1522 1523 // We have three cases : 1524 // 1) The first line after the Dn is a "control:" 1525 // 2) The first line after the Dn is a "changeType:" 1526 // 3) The first line after the Dn is anything else 1527 if ( lowerLine.startsWith( "control:" ) ) 1528 { 1529 if ( containsEntries ) 1530 { 1531 LOG.error( I18n.err( I18n.ERR_13401_CHANGE_NOT_ALLOWED, lineNumber ) ); 1532 throw new LdapLdifException( I18n.err( I18n.ERR_13440_NO_CHANGE ) ); 1533 } 1534 1535 containsChanges = true; 1536 1537 if ( controlSeen ) 1538 { 1539 LOG.error( I18n.err( I18n.ERR_13418_CONTROL_ALREADY_FOUND, lineNumber ) ); 1540 throw new LdapLdifException( I18n.err( I18n.ERR_13457_MISPLACED_CONTROL ) ); 1541 } 1542 1543 // Parse the control 1544 control = parseControl( line.substring( "control:".length() ) ); 1545 entry.addControl( control ); 1546 } 1547 else if ( lowerLine.startsWith( "changetype:" ) ) 1548 { 1549 if ( containsEntries ) 1550 { 1551 LOG.error( I18n.err( I18n.ERR_13401_CHANGE_NOT_ALLOWED, lineNumber ) ); 1552 throw new LdapLdifException( I18n.err( I18n.ERR_13440_NO_CHANGE ) ); 1553 } 1554 1555 containsChanges = true; 1556 1557 if ( changeTypeSeen ) 1558 { 1559 LOG.error( I18n.err( I18n.ERR_13419_CHANGETYPE_ALREADY_FOUND, lineNumber ) ); 1560 throw new LdapLdifException( I18n.err( I18n.ERR_13458_MISPLACED_CHANGETYPE ) ); 1561 } 1562 1563 // A change request 1564 type = CHANGE; 1565 controlSeen = true; 1566 1567 operation = parseChangeType( line ); 1568 1569 // Parse the change operation in a separate function 1570 parseChange( entry, iter, operation ); 1571 changeTypeSeen = true; 1572 } 1573 else if ( line.indexOf( ':' ) > 0 ) 1574 { 1575 if ( containsChanges ) 1576 { 1577 LOG.error( I18n.err( I18n.ERR_13401_CHANGE_NOT_ALLOWED, lineNumber ) ); 1578 throw new LdapLdifException( I18n.err( I18n.ERR_13440_NO_CHANGE ) ); 1579 } 1580 1581 containsEntries = true; 1582 1583 if ( controlSeen || changeTypeSeen ) 1584 { 1585 LOG.error( I18n.err( I18n.ERR_13420_AT_VALUE_NOT_ALLOWED_AFTER_CONTROL, lineNumber ) ); 1586 throw new LdapLdifException( I18n.err( I18n.ERR_13459_MISPLACED_ATTRIBUTETYPE ) ); 1587 } 1588 1589 parseAttributeValue( entry, line, lowerLine ); 1590 type = LDIF_ENTRY; 1591 } 1592 else 1593 { 1594 // Invalid attribute Value 1595 LOG.error( I18n.err( I18n.ERR_13421_ATTRIBUTE_TYPE_EXPECTED, lineNumber ) ); 1596 throw new LdapLdifException( I18n.err( I18n.ERR_13460_BAD_ATTRIBUTE ) ); 1597 } 1598 } 1599 1600 if ( type == LDIF_ENTRY ) 1601 { 1602 if ( LOG.isDebugEnabled() ) 1603 { 1604 LOG.debug( I18n.msg( I18n.MSG_13406_READ_ENTRY, entry ) ); 1605 } 1606 } 1607 else if ( type == CHANGE ) 1608 { 1609 entry.setChangeType( operation ); 1610 1611 if ( LOG.isDebugEnabled() ) 1612 { 1613 LOG.debug( I18n.msg( I18n.MSG_13404_READ_MODIF, entry ) ); 1614 } 1615 } 1616 else 1617 { 1618 LOG.error( I18n.err( I18n.ERR_13422_UNKNOWN_ENTRY_TYPE, lineNumber ) ); 1619 throw new LdapLdifException( I18n.err( I18n.ERR_13461_UNKNOWN_ENTRY ) ); 1620 } 1621 1622 return entry; 1623 } 1624 1625 1626 /** 1627 * Parse the version from the ldif input. 1628 * 1629 * @return A number representing the version (default to 1) 1630 * @throws LdapLdifException If the version is incorrect or if the input is incorrect 1631 */ 1632 protected int parseVersion() throws LdapLdifException 1633 { 1634 int ver = DEFAULT_VERSION; 1635 1636 // First, read a list of lines 1637 readLines(); 1638 1639 if ( lines.isEmpty() ) 1640 { 1641 if ( LOG.isWarnEnabled() ) 1642 { 1643 LOG.warn( I18n.msg( I18n.MSG_13414_LDIF_FILE_EMPTY ) ); 1644 } 1645 1646 return ver; 1647 } 1648 1649 // get the first line 1650 String line = lines.get( 0 ); 1651 1652 // <ldif-file> ::= "version:" <fill> <number> 1653 char[] document = line.toCharArray(); 1654 String versionNumber; 1655 1656 if ( line.startsWith( "version:" ) ) 1657 { 1658 position += "version:".length(); 1659 parseFill( document ); 1660 1661 // Version number. Must be '1' in this version 1662 versionNumber = parseNumber( document ); 1663 1664 // We should not have any other chars after the number 1665 if ( position != document.length ) 1666 { 1667 LOG.error( I18n.err( I18n.ERR_13423_VERSION_NOT_A_NUMBER, lineNumber ) ); 1668 throw new LdapLdifException( I18n.err( I18n.ERR_13462_LDIF_PARSING_ERROR ) ); 1669 } 1670 1671 try 1672 { 1673 ver = Integer.parseInt( versionNumber ); 1674 } 1675 catch ( NumberFormatException nfe ) 1676 { 1677 LOG.error( I18n.err( I18n.ERR_13423_VERSION_NOT_A_NUMBER, lineNumber ) ); 1678 throw new LdapLdifException( I18n.err( I18n.ERR_13462_LDIF_PARSING_ERROR ), nfe ); 1679 } 1680 1681 if ( LOG.isDebugEnabled() ) 1682 { 1683 LOG.debug( I18n.msg( I18n.MSG_13400_LDIF_VERSION, versionNumber ) ); 1684 } 1685 1686 // We have found the version, just discard the line from the list 1687 lines.remove( 0 ); 1688 1689 // and read the next lines if the current buffer is empty 1690 if ( lines.isEmpty() ) 1691 { 1692 // include the version line as part of the first entry 1693 int tmpEntryLen = entryLen; 1694 1695 readLines(); 1696 1697 entryLen += tmpEntryLen; 1698 } 1699 } 1700 else 1701 { 1702 if ( LOG.isInfoEnabled() ) 1703 { 1704 LOG.info( I18n.msg( I18n.MSG_13413_NO_VERSION_ASSUMING_1 ) ); 1705 } 1706 } 1707 1708 return ver; 1709 } 1710 1711 1712 /** 1713 * gets a line from the underlying data store 1714 * 1715 * @return a line of characters or null if EOF reached 1716 * @throws IOException on read failure 1717 */ 1718 protected String getLine() throws IOException 1719 { 1720 return ( ( BufferedReader ) reader ).readLine(); 1721 } 1722 1723 1724 /** 1725 * Reads an entry in a ldif buffer, and returns the resulting lines, without 1726 * comments, and unfolded. 1727 * 1728 * The lines represent *one* entry. 1729 * 1730 * @throws LdapLdifException If something went wrong 1731 */ 1732 protected void readLines() throws LdapLdifException 1733 { 1734 String line; 1735 boolean insideComment = true; 1736 boolean isFirstLine = true; 1737 1738 lines.clear(); 1739 entryLen = 0; 1740 entryOffset = offset; 1741 1742 StringBuilder sb = new StringBuilder(); 1743 1744 try 1745 { 1746 while ( ( line = getLine() ) != null ) 1747 { 1748 lineNumber++; 1749 1750 if ( line.length() == 0 ) 1751 { 1752 if ( isFirstLine ) 1753 { 1754 continue; 1755 } 1756 else 1757 { 1758 // The line is empty, we have read an entry 1759 insideComment = false; 1760 offset++; 1761 break; 1762 } 1763 } 1764 1765 // We will read the first line which is not a comment 1766 switch ( line.charAt( 0 ) ) 1767 { 1768 case '#': 1769 insideComment = true; 1770 break; 1771 1772 case ' ': 1773 isFirstLine = false; 1774 1775 if ( insideComment ) 1776 { 1777 continue; 1778 } 1779 else if ( sb.length() == 0 ) 1780 { 1781 LOG.error( I18n.err( I18n.ERR_13424_EMPTY_CONTINUATION_LINE, lineNumber ) ); 1782 throw new LdapLdifException( I18n.err( I18n.ERR_13462_LDIF_PARSING_ERROR ) ); 1783 } 1784 else 1785 { 1786 sb.append( line.substring( 1 ) ); 1787 } 1788 1789 insideComment = false; 1790 break; 1791 1792 default: 1793 isFirstLine = false; 1794 1795 // We have found a new entry 1796 // First, stores the previous one if any. 1797 if ( sb.length() != 0 ) 1798 { 1799 lines.add( sb.toString() ); 1800 } 1801 1802 sb = new StringBuilder( line ); 1803 insideComment = false; 1804 break; 1805 } 1806 1807 byte[] data = Strings.getBytesUtf8( line ); 1808 // FIXME might fail on windows in the new line issue, yet to check 1809 offset += ( data.length + 1 ); 1810 entryLen += ( data.length + 1 ); 1811 } 1812 } 1813 catch ( IOException ioe ) 1814 { 1815 throw new LdapLdifException( I18n.err( I18n.ERR_13463_ERROR_WHILE_READING_LDIF_LINE ), ioe ); 1816 } 1817 1818 // Stores the current line if necessary. 1819 if ( sb.length() != 0 ) 1820 { 1821 lines.add( sb.toString() ); 1822 } 1823 } 1824 1825 1826 /** 1827 * Parse a ldif file (using the default encoding). 1828 * 1829 * @param fileName The ldif file 1830 * @return A list of entries 1831 * @throws LdapLdifException If the parsing fails 1832 */ 1833 public List<LdifEntry> parseLdifFile( String fileName ) throws LdapLdifException 1834 { 1835 return parseLdifFile( fileName, Strings.getDefaultCharsetName() ); 1836 } 1837 1838 1839 /** 1840 * Parse a ldif file, decoding it using the given charset encoding 1841 * 1842 * @param fileName The ldif file 1843 * @param encoding The charset encoding to use 1844 * @return A list of entries 1845 * @throws LdapLdifException If the parsing fails 1846 */ 1847 public List<LdifEntry> parseLdifFile( String fileName, String encoding ) throws LdapLdifException 1848 { 1849 if ( Strings.isEmpty( fileName ) ) 1850 { 1851 String msg = I18n.err( I18n.ERR_13425_EMPTY_FILE_NAME ); 1852 LOG.error( msg ); 1853 throw new LdapLdifException( msg ); 1854 } 1855 1856 File file = new File( fileName ); 1857 1858 if ( !file.exists() ) 1859 { 1860 LOG.error( I18n.err( I18n.ERR_13426_CANNOT_PARSE_INEXISTANT_FILE, fileName ) ); 1861 throw new LdapLdifException( I18n.err( I18n.ERR_13464_FILENAME_NOT_FOUND, fileName ) ); 1862 } 1863 1864 // Open the file and then get a channel from the stream 1865 try ( InputStream is = Files.newInputStream( Paths.get( fileName ) ); 1866 BufferedReader bufferReader = new BufferedReader( 1867 new InputStreamReader( is, Charset.forName( encoding ) ) ) ) 1868 { 1869 return parseLdif( bufferReader ); 1870 } 1871 catch ( FileNotFoundException fnfe ) 1872 { 1873 LOG.error( I18n.err( I18n.ERR_13427_CANNOT_FIND_FILE, fileName ) ); 1874 throw new LdapLdifException( I18n.err( I18n.ERR_13464_FILENAME_NOT_FOUND, fileName ), fnfe ); 1875 } 1876 catch ( LdapException le ) 1877 { 1878 throw new LdapLdifException( le.getMessage(), le ); 1879 } 1880 catch ( IOException ioe ) 1881 { 1882 throw new LdapLdifException( ioe.getMessage(), ioe ); 1883 } 1884 } 1885 1886 1887 /** 1888 * A method which parses a ldif string and returns a list of entries. 1889 * 1890 * @param ldif The ldif string 1891 * @return A list of entries, or an empty List 1892 * @throws LdapLdifException If something went wrong 1893 */ 1894 public List<LdifEntry> parseLdif( String ldif ) throws LdapLdifException 1895 { 1896 if ( LOG.isDebugEnabled() ) 1897 { 1898 LOG.debug( I18n.msg( I18n.MSG_13407_STARTS_PARSING_LDIF ) ); 1899 } 1900 1901 if ( Strings.isEmpty( ldif ) ) 1902 { 1903 return new ArrayList<>(); 1904 } 1905 1906 try ( BufferedReader bufferReader = new BufferedReader( new StringReader( ldif ) ) ) 1907 { 1908 List<LdifEntry> entries = parseLdif( bufferReader ); 1909 1910 if ( LOG.isDebugEnabled() ) 1911 { 1912 LOG.debug( I18n.msg( I18n.MSG_13403_PARSED_N_ENTRIES, Integer.valueOf( entries.size() ) ) ); 1913 } 1914 1915 return entries; 1916 } 1917 catch ( LdapLdifException ne ) 1918 { 1919 LOG.error( I18n.err( I18n.ERR_13428_CANNOT_PARSE_LDIF, ne.getLocalizedMessage() ) ); 1920 throw new LdapLdifException( I18n.err( I18n.ERR_13442_ERROR_PARSING_LDIF_BUFFER ), ne ); 1921 } 1922 catch ( LdapException le ) 1923 { 1924 throw new LdapLdifException( le.getMessage(), le ); 1925 } 1926 catch ( IOException ioe ) 1927 { 1928 throw new LdapLdifException( I18n.err( I18n.ERR_13450_CANNOT_CLOSE_FILE ), ioe ); 1929 } 1930 } 1931 1932 1933 // ------------------------------------------------------------------------ 1934 // Iterator Methods 1935 // ------------------------------------------------------------------------ 1936 /** 1937 * Gets the next LDIF on the channel. 1938 * 1939 * @return the next LDIF as a String. 1940 */ 1941 private LdifEntry nextInternal() 1942 { 1943 try 1944 { 1945 if ( LOG.isDebugEnabled() ) 1946 { 1947 LOG.debug( I18n.msg( I18n.MSG_13411_NEXT_CALLED ) ); 1948 } 1949 1950 LdifEntry entry = prefetched; 1951 readLines(); 1952 1953 try 1954 { 1955 prefetched = parseEntry(); 1956 } 1957 catch ( LdapLdifException ne ) 1958 { 1959 error = ne; 1960 throw new NoSuchElementException( ne.getMessage() ); 1961 } 1962 catch ( LdapException le ) 1963 { 1964 throw new NoSuchElementException( le.getMessage() ); 1965 } 1966 1967 if ( LOG.isDebugEnabled() ) 1968 { 1969 LOG.debug( I18n.msg( I18n.MSG_13412_NEXT_RETURNING_LDIF, entry ) ); 1970 } 1971 1972 return entry; 1973 } 1974 catch ( LdapLdifException ne ) 1975 { 1976 LOG.error( I18n.err( I18n.ERR_13430_PREMATURE_LDIF_ITERATOR_TERMINATION ) ); 1977 error = ne; 1978 return null; 1979 } 1980 } 1981 1982 1983 /** 1984 * Gets the next LDIF on the channel. 1985 * 1986 * @return the next LDIF as a String. 1987 */ 1988 public LdifEntry next() 1989 { 1990 return nextInternal(); 1991 } 1992 1993 1994 /** 1995 * Gets the current entry, but don't move forward. 1996 * 1997 * @return the pre-fetched entry 1998 */ 1999 public LdifEntry fetch() 2000 { 2001 return prefetched; 2002 } 2003 2004 2005 /** 2006 * Tests to see if another LDIF is on the input channel. 2007 * 2008 * @return true if another LDIF is available false otherwise. 2009 */ 2010 private boolean hasNextInternal() 2011 { 2012 return null != prefetched; 2013 } 2014 2015 2016 /** 2017 * Tests to see if another LDIF is on the input channel. 2018 * 2019 * @return true if another LDIF is available false otherwise. 2020 */ 2021 public boolean hasNext() 2022 { 2023 if ( LOG.isDebugEnabled() ) 2024 { 2025 if ( prefetched != null ) 2026 { 2027 LOG.debug( I18n.msg( I18n.MSG_13410_HAS_NEXT_TRUE ) ); 2028 } 2029 else 2030 { 2031 LOG.debug( I18n.msg( I18n.MSG_13409_HAS_NEXT_FALSE ) ); 2032 } 2033 } 2034 2035 return hasNextInternal(); 2036 } 2037 2038 2039 /** 2040 * Always throws UnsupportedOperationException! 2041 * 2042 * @see java.util.Iterator#remove() 2043 */ 2044 private void removeInternal() 2045 { 2046 throw new UnsupportedOperationException(); 2047 } 2048 2049 2050 /** 2051 * Always throws UnsupportedOperationException! 2052 * 2053 * @see java.util.Iterator#remove() 2054 */ 2055 public void remove() 2056 { 2057 removeInternal(); 2058 } 2059 2060 2061 /** 2062 * @return An iterator on the file 2063 */ 2064 @Override 2065 public Iterator<LdifEntry> iterator() 2066 { 2067 return new Iterator<LdifEntry>() 2068 { 2069 @Override 2070 public boolean hasNext() 2071 { 2072 return hasNextInternal(); 2073 } 2074 2075 2076 @Override 2077 public LdifEntry next() 2078 { 2079 try 2080 { 2081 return nextInternal(); 2082 } 2083 catch ( NoSuchElementException nse ) 2084 { 2085 LOG.error( nse.getMessage() ); 2086 return null; 2087 } 2088 } 2089 2090 2091 @Override 2092 public void remove() 2093 { 2094 throw new UnsupportedOperationException(); 2095 } 2096 }; 2097 } 2098 2099 2100 /** 2101 * @return True if an error occurred during parsing 2102 */ 2103 public boolean hasError() 2104 { 2105 return error != null; 2106 } 2107 2108 2109 /** 2110 * @return The exception that occurs during an entry parsing 2111 */ 2112 public Exception getError() 2113 { 2114 return error; 2115 } 2116 2117 2118 /** 2119 * The main entry point of the LdifParser. It reads a buffer and returns a 2120 * List of entries. 2121 * 2122 * @param reader The buffer being processed 2123 * @return A list of entries 2124 * @throws LdapException If something went wrong 2125 */ 2126 public List<LdifEntry> parseLdif( BufferedReader reader ) throws LdapException 2127 { 2128 // Create a list that will contain the read entries 2129 List<LdifEntry> entries = new ArrayList<>(); 2130 2131 this.reader = reader; 2132 2133 // First get the version - if any - 2134 version = parseVersion(); 2135 prefetched = parseEntry(); 2136 2137 // When done, get the entries one by one. 2138 for ( LdifEntry entry : this ) 2139 { 2140 if ( entry != null ) 2141 { 2142 entries.add( entry ); 2143 } 2144 else 2145 { 2146 throw new LdapLdifException( I18n.err( I18n.ERR_13429_ERROR_PARSING_LDIF, error.getLocalizedMessage() ) ); 2147 } 2148 } 2149 2150 return entries; 2151 } 2152 2153 2154 /** 2155 * @return True if the ldif file contains entries, fals if it contains changes 2156 */ 2157 public boolean containsEntries() 2158 { 2159 return containsEntries; 2160 } 2161 2162 2163 /** 2164 * @return the current line that is being processed by the reader 2165 */ 2166 public int getLineNumber() 2167 { 2168 return lineNumber; 2169 } 2170 2171 2172 /** 2173 * Creates a schema aware LdifEntry 2174 * 2175 * @param schemaManager The SchemaManager 2176 * @return an LdifEntry that is schema aware 2177 */ 2178 protected LdifEntry createLdifEntry( SchemaManager schemaManager ) 2179 { 2180 if ( schemaManager != null ) 2181 { 2182 return new LdifEntry( schemaManager ); 2183 } 2184 else 2185 { 2186 return new LdifEntry(); 2187 } 2188 } 2189 2190 2191 /** 2192 * @return true if the DN validation is turned on 2193 */ 2194 public boolean isValidateDn() 2195 { 2196 return validateDn; 2197 } 2198 2199 2200 /** 2201 * Turns on/off the DN validation 2202 * 2203 * @param validateDn the boolean flag 2204 */ 2205 public void setValidateDn( boolean validateDn ) 2206 { 2207 this.validateDn = validateDn; 2208 } 2209 2210 2211 /** 2212 * @param schemaManager the schemaManager to set 2213 */ 2214 public void setSchemaManager( SchemaManager schemaManager ) 2215 { 2216 this.schemaManager = schemaManager; 2217 } 2218 2219 2220 /** 2221 * {@inheritDoc} 2222 */ 2223 @Override 2224 public void close() throws IOException 2225 { 2226 if ( reader != null ) 2227 { 2228 position = 0; 2229 reader.close(); 2230 containsEntries = false; 2231 containsChanges = false; 2232 offset = 0; 2233 entryOffset = 0; 2234 lineNumber = 0; 2235 } 2236 } 2237}