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.entry; 021 022 023import java.text.ParseException; 024import java.util.Arrays; 025import java.util.Iterator; 026 027import javax.naming.NamingEnumeration; 028import javax.naming.NamingException; 029import javax.naming.directory.Attributes; 030import javax.naming.directory.BasicAttribute; 031import javax.naming.directory.BasicAttributes; 032 033import org.apache.directory.api.i18n.I18n; 034import org.apache.directory.api.ldap.model.exception.LdapException; 035import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeTypeException; 036import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException; 037import org.apache.directory.api.ldap.model.name.Dn; 038import org.apache.directory.api.util.Chars; 039import org.apache.directory.api.util.Position; 040import org.apache.directory.api.util.Strings; 041 042 043/** 044 * A set of utility fuctions for working with Attributes. 045 * 046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 047 */ 048public final class AttributeUtils 049{ 050 private AttributeUtils() 051 { 052 } 053 054 055 /** 056 * Check if an attribute contains a value. The test is case insensitive, 057 * and the value is supposed to be a String. If the value is a byte[], 058 * then the case sensitivity is useless. 059 * 060 * @param attr The attribute to check 061 * @param value The value to look for 062 * @return true if the value is present in the attribute 063 */ 064 public static boolean containsValueCaseIgnore( javax.naming.directory.Attribute attr, Object value ) 065 { 066 // quick bypass test 067 if ( attr.contains( value ) ) 068 { 069 return true; 070 } 071 072 try 073 { 074 if ( value instanceof String ) 075 { 076 String strVal = ( String ) value; 077 078 NamingEnumeration<?> attrVals = attr.getAll(); 079 080 while ( attrVals.hasMoreElements() ) 081 { 082 Object attrVal = attrVals.nextElement(); 083 084 if ( attrVal instanceof String && strVal.equalsIgnoreCase( ( String ) attrVal ) ) 085 { 086 return true; 087 } 088 } 089 } 090 else 091 { 092 byte[] valueBytes = ( byte[] ) value; 093 094 NamingEnumeration<?> attrVals = attr.getAll(); 095 096 while ( attrVals.hasMoreElements() ) 097 { 098 Object attrVal = attrVals.nextElement(); 099 100 if ( attrVal instanceof byte[] && Arrays.equals( ( byte[] ) attrVal, valueBytes ) ) 101 { 102 return true; 103 } 104 } 105 } 106 } 107 catch ( NamingException ne ) 108 { 109 return false; 110 } 111 112 return false; 113 } 114 115 116 /** 117 * Check if the attributes is a BasicAttributes, and if so, switch 118 * the case sensitivity to false to avoid tricky problems in the server. 119 * (Ldap attributeTypes are *always* case insensitive) 120 * 121 * @param attributes The Attributes to check 122 * @return The modified Attributes 123 */ 124 public static Attributes toCaseInsensitive( Attributes attributes ) 125 { 126 if ( attributes == null ) 127 { 128 return attributes; 129 } 130 131 if ( attributes instanceof BasicAttributes ) 132 { 133 if ( attributes.isCaseIgnored() ) 134 { 135 // Just do nothing if the Attributes is already case insensitive 136 return attributes; 137 } 138 else 139 { 140 // Ok, bad news : we have to create a new BasicAttributes 141 // which will be case insensitive 142 Attributes newAttrs = new BasicAttributes( true ); 143 144 NamingEnumeration<?> attrs = attributes.getAll(); 145 146 if ( attrs != null ) 147 { 148 // Iterate through the attributes now 149 while ( attrs.hasMoreElements() ) 150 { 151 newAttrs.put( ( javax.naming.directory.Attribute ) attrs.nextElement() ); 152 } 153 } 154 155 return newAttrs; 156 } 157 } 158 else 159 { 160 // we can safely return the attributes if it's not a BasicAttributes 161 return attributes; 162 } 163 } 164 165 166 /** 167 * Parse attribute's options : 168 * 169 * <pre> 170 * options = *( ';' option ) 171 * option = 1*keychar 172 * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-' 173 * </pre> 174 * 175 * @param str The parsed option 176 * @param pos The position in the parsed option string 177 * @exception ParseException The parsed option is invalid 178 */ 179 private static void parseOptions( char[] str, Position pos ) throws ParseException 180 { 181 while ( Strings.isCharASCII( str, pos.start, ';' ) ) 182 { 183 pos.start++; 184 185 // We have an option 186 if ( !Chars.isAlphaDigitMinus( str, pos.start ) ) 187 { 188 // We must have at least one keychar 189 throw new ParseException( I18n.err( I18n.ERR_13201_EMPTY_OPTION_NOT_ALLOWED ), pos.start ); 190 } 191 192 pos.start++; 193 194 while ( Chars.isAlphaDigitMinus( str, pos.start ) ) 195 { 196 pos.start++; 197 } 198 } 199 } 200 201 202 203 204 /** 205 * Parse attribute's options : 206 * 207 * <pre> 208 * options = *( ';' option ) 209 * option = 1*keychar 210 * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-' 211 * </pre> 212 * 213 * @param bytes The parsed option 214 * @param pos The position in the parsed option bytes 215 * @exception ParseException The parsed option is invalid 216 */ 217 private static void parseOptions( byte[] bytes, Position pos ) throws ParseException 218 { 219 while ( Strings.isCharASCII( bytes, pos.start, ';' ) ) 220 { 221 pos.start++; 222 223 // We have an option 224 if ( !Chars.isAlphaDigitMinus( bytes, pos.start ) ) 225 { 226 // We must have at least one keychar 227 throw new ParseException( I18n.err( I18n.ERR_13201_EMPTY_OPTION_NOT_ALLOWED ), pos.start ); 228 } 229 230 pos.start++; 231 232 while ( Chars.isAlphaDigitMinus( bytes, pos.start ) ) 233 { 234 pos.start++; 235 } 236 } 237 } 238 239 240 /** 241 * Parse a number : 242 * 243 * <pre> 244 * number = '0' | '1'..'9' digits 245 * digits = '0'..'9'* 246 * </pre> 247 * 248 * @param filter The number in the filter 249 * @param pos The position in the parsed filter string 250 * @return true if a number has been found 251 */ 252 private static boolean parseNumber( char[] filter, Position pos ) 253 { 254 char c = Strings.charAt( filter, pos.start ); 255 256 switch ( c ) 257 { 258 case '0': 259 // If we get a starting '0', we should get out 260 pos.start++; 261 return true; 262 263 case '1': 264 case '2': 265 case '3': 266 case '4': 267 case '5': 268 case '6': 269 case '7': 270 case '8': 271 case '9': 272 pos.start++; 273 break; 274 275 default: 276 // Not a number. 277 return false; 278 } 279 280 while ( Chars.isDigit( filter, pos.start ) ) 281 { 282 pos.start++; 283 } 284 285 return true; 286 } 287 288 289 /** 290 * Parse a number : 291 * 292 * <pre> 293 * number = '0' | '1'..'9' digits 294 * digits = '0'..'9'* 295 * </pre> 296 * 297 * @param bytes The parsed number 298 * @param pos The position in the parsed number string 299 * @return true if a number has been found 300 */ 301 private static boolean parseNumber( byte[] bytes, Position pos ) 302 { 303 byte b = Strings.byteAt( bytes, pos.start ); 304 305 switch ( b ) 306 { 307 case '0': 308 // If we get a starting '0', we should get out 309 pos.start++; 310 return true; 311 312 case '1': 313 case '2': 314 case '3': 315 case '4': 316 case '5': 317 case '6': 318 case '7': 319 case '8': 320 case '9': 321 pos.start++; 322 break; 323 324 default: 325 // Not a number. 326 return false; 327 } 328 329 while ( Chars.isDigit( bytes, pos.start ) ) 330 { 331 pos.start++; 332 } 333 334 return true; 335 } 336 337 338 /** 339 * Parse an OID. 340 * 341 * numericoid = number 1*( '.' number ) 342 * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' ) 343 * 344 * @param str The OID to parse 345 * @param pos The current position in the string 346 * @throws ParseException If we don't have a valid OID 347 */ 348 private static void parseOID( char[] str, Position pos ) throws ParseException 349 { 350 // We have an OID 351 parseNumber( str, pos ); 352 353 // We must have at least one '.' number 354 if ( !Strings.isCharASCII( str, pos.start, '.' ) ) 355 { 356 throw new ParseException( I18n.err( I18n.ERR_13221_INVALID_OID_MISSING_DOT ), pos.start ); 357 } 358 359 pos.start++; 360 361 if ( !parseNumber( str, pos ) ) 362 { 363 throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start ); 364 } 365 366 while ( true ) 367 { 368 // Break if we get something which is not a '.' 369 if ( !Strings.isCharASCII( str, pos.start, '.' ) ) 370 { 371 break; 372 } 373 374 pos.start++; 375 376 if ( !parseNumber( str, pos ) ) 377 { 378 throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start ); 379 } 380 } 381 } 382 383 384 385 386 /** 387 * Parse an OID. 388 * 389 * numericoid = number 1*( '.' number ) 390 * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' ) 391 * 392 * @param bytes The OID to parse 393 * @param pos The current position in the string 394 * @throws ParseException If we don't have a valid OID 395 */ 396 private static void parseOID( byte[] bytes, Position pos ) throws ParseException 397 { 398 // We have an OID 399 parseNumber( bytes, pos ); 400 401 // We must have at least one '.' number 402 if ( !Strings.isCharASCII( bytes, pos.start, '.' ) ) 403 { 404 throw new ParseException( I18n.err( I18n.ERR_13221_INVALID_OID_MISSING_DOT ), pos.start ); 405 } 406 407 pos.start++; 408 409 if ( !parseNumber( bytes, pos ) ) 410 { 411 throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start ); 412 } 413 414 while ( true ) 415 { 416 // Break if we get something which is not a '.' 417 if ( !Strings.isCharASCII( bytes, pos.start, '.' ) ) 418 { 419 break; 420 } 421 422 pos.start++; 423 424 if ( !parseNumber( bytes, pos ) ) 425 { 426 throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start ); 427 } 428 } 429 } 430 431 432 /** 433 * Parse an attribute. The grammar is : 434 * attributedescription = attributetype options 435 * attributetype = oid 436 * oid = descr / numericoid 437 * descr = keystring 438 * numericoid = number 1*( '.' number ) 439 * options = *( ';' option ) 440 * option = 1*keychar 441 * keystring = leadkeychar *keychar 442 * leadkeychar = 'a'-z' | 'A'-'Z' 443 * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-' 444 * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' ) 445 * 446 * @param str The parsed attribute, 447 * @param pos The position of the attribute in the current string 448 * @param withOption A flag set if we want to parse the options 449 * @param relaxed A flag set if we want to parse without being too strict 450 * @return The parsed attribute if valid 451 * @throws ParseException If we had an issue while parsing the attribute 452 */ 453 public static String parseAttribute( char[] str, Position pos, boolean withOption, boolean relaxed ) 454 throws ParseException 455 { 456 // We must have an OID or an DESCR first 457 char c = Strings.charAt( str, pos.start ); 458 459 if ( c == '\0' ) 460 { 461 throw new ParseException( I18n.err( I18n.ERR_13222_EMPTY_ATTRIBUTE ), pos.start ); 462 } 463 464 int start = pos.start; 465 466 if ( Chars.isAlpha( c ) ) 467 { 468 // A DESCR 469 pos.start++; 470 471 while ( Chars.isAlphaDigitMinus( str, pos.start ) || ( relaxed && Chars.isCharASCII( str, pos.start, '_' ) ) ) 472 { 473 pos.start++; 474 } 475 476 // Parse the options if needed 477 if ( withOption ) 478 { 479 parseOptions( str, pos ); 480 } 481 482 return new String( str, start, pos.start - start ); 483 } 484 else if ( Chars.isDigit( c ) ) 485 { 486 // An OID 487 pos.start++; 488 489 // Parse the OID 490 parseOID( str, pos ); 491 492 // Parse the options 493 if ( withOption ) 494 { 495 parseOptions( str, pos ); 496 } 497 498 return new String( str, start, pos.start - start ); 499 } 500 else 501 { 502 throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos.start ); 503 } 504 } 505 506 507 508 509 /** 510 * Parse an attribute. The grammar is : 511 * attributedescription = attributetype options 512 * attributetype = oid 513 * oid = descr / numericoid 514 * descr = keystring 515 * numericoid = number 1*( '.' number ) 516 * options = *( ';' option ) 517 * option = 1*keychar 518 * keystring = leadkeychar *keychar 519 * leadkeychar = 'a'-z' | 'A'-'Z' 520 * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-' 521 * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' ) 522 * 523 * @param bytes The parsed attribute, 524 * @param pos The position of the attribute in the current string 525 * @param withOption A flag set if we want to parse the options 526 * @param relaxed A flag set if we want to parse without being too strict 527 * @return The parsed attribute if valid 528 * @throws ParseException If we had an issue while parsing the attribute 529 */ 530 public static String parseAttribute( byte[] bytes, Position pos, boolean withOption, boolean relaxed ) 531 throws ParseException 532 { 533 // We must have an OID or an DESCR first 534 byte b = Strings.byteAt( bytes, pos.start ); 535 536 if ( b == '\0' ) 537 { 538 throw new ParseException( I18n.err( I18n.ERR_13222_EMPTY_ATTRIBUTE ), pos.start ); 539 } 540 541 int start = pos.start; 542 543 if ( Chars.isAlpha( b ) ) 544 { 545 // A DESCR 546 while ( Chars.isAlphaDigitMinus( bytes, pos.start ) || ( relaxed && Strings.isCharASCII( bytes, pos.start, '_' ) ) ) 547 { 548 pos.start++; 549 } 550 551 // Parse the options if needed 552 if ( withOption ) 553 { 554 parseOptions( bytes, pos ); 555 } 556 557 return Strings.utf8ToString( bytes, start, pos.start - start ); 558 } 559 else if ( Chars.isDigit( b ) ) 560 { 561 // Parse the OID 562 parseOID( bytes, pos ); 563 564 // Parse the options 565 if ( withOption ) 566 { 567 parseOptions( bytes, pos ); 568 } 569 570 return Strings.utf8ToString( bytes, start, pos.start - start ); 571 } 572 else 573 { 574 throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos.start ); 575 } 576 } 577 578 579 /** 580 * A method to apply a modification to an existing entry. 581 * 582 * @param entry The entry on which we want to apply a modification 583 * @param modification the Modification to be applied 584 * @throws LdapException if some operation fails. 585 */ 586 public static void applyModification( Entry entry, Modification modification ) throws LdapException 587 { 588 Attribute modAttr = modification.getAttribute(); 589 String modificationId = modAttr.getUpId(); 590 591 switch ( modification.getOperation() ) 592 { 593 case ADD_ATTRIBUTE: 594 Attribute modifiedAttr = entry.get( modificationId ); 595 596 if ( modifiedAttr == null ) 597 { 598 // The attribute should be added. 599 entry.put( modAttr ); 600 } 601 else 602 { 603 // The attribute exists : the values can be different, 604 // so we will just add the new values to the existing ones. 605 for ( Value value : modAttr ) 606 { 607 // If the value already exist, nothing is done. 608 // Note that the attribute *must* have been 609 // normalized before. 610 modifiedAttr.add( value ); 611 } 612 } 613 614 break; 615 616 case REMOVE_ATTRIBUTE: 617 if ( modAttr.get() == null ) 618 { 619 // We have no value in the ModificationItem attribute : 620 // we have to remove the whole attribute from the initial 621 // entry 622 entry.removeAttributes( modificationId ); 623 } 624 else 625 { 626 // We just have to remove the values from the original 627 // entry, if they exist. 628 modifiedAttr = entry.get( modificationId ); 629 630 if ( modifiedAttr == null ) 631 { 632 break; 633 } 634 635 for ( Value value : modAttr ) 636 { 637 // If the value does not exist, nothing is done. 638 // Note that the attribute *must* have been 639 // normalized before. 640 modifiedAttr.remove( value ); 641 } 642 643 if ( modifiedAttr.size() == 0 ) 644 { 645 // If this was the last value, remove the attribute 646 entry.removeAttributes( modifiedAttr.getUpId() ); 647 } 648 } 649 650 break; 651 652 case REPLACE_ATTRIBUTE: 653 if ( modAttr.get() == null ) 654 { 655 // If the modification does not have any value, we have 656 // to delete the attribute from the entry. 657 entry.removeAttributes( modificationId ); 658 } 659 else 660 { 661 // otherwise, just substitute the existing attribute. 662 entry.put( modAttr ); 663 } 664 665 break; 666 default: 667 break; 668 } 669 } 670 671 672 /** 673 * Convert a BasicAttributes or a AttributesImpl to an Entry 674 * 675 * @param attributes the BasicAttributes or AttributesImpl instance to convert 676 * @param dn The Dn which is needed by the Entry 677 * @return An instance of a Entry object 678 * 679 * @throws LdapException If we get an invalid attribute 680 */ 681 public static Entry toEntry( Attributes attributes, Dn dn ) throws LdapException 682 { 683 if ( attributes instanceof BasicAttributes ) 684 { 685 try 686 { 687 Entry entry = new DefaultEntry( dn ); 688 689 for ( NamingEnumeration<? extends javax.naming.directory.Attribute> attrs = attributes.getAll(); attrs 690 .hasMoreElements(); ) 691 { 692 javax.naming.directory.Attribute attr = attrs.nextElement(); 693 694 Attribute entryAttribute = toApiAttribute( attr ); 695 696 if ( entryAttribute != null ) 697 { 698 entry.put( entryAttribute ); 699 } 700 } 701 702 return entry; 703 } 704 catch ( LdapException ne ) 705 { 706 throw new LdapInvalidAttributeTypeException( ne.getMessage(), ne ); 707 } 708 } 709 else 710 { 711 return null; 712 } 713 } 714 715 716 /** 717 * Converts an {@link Entry} to an {@link Attributes}. 718 * 719 * @param entry 720 * the {@link Entry} to convert 721 * @return 722 * the equivalent {@link Attributes} 723 */ 724 public static Attributes toAttributes( Entry entry ) 725 { 726 if ( entry != null ) 727 { 728 Attributes attributes = new BasicAttributes( true ); 729 730 // Looping on attributes 731 for ( Iterator<Attribute> attributeIterator = entry.iterator(); attributeIterator.hasNext(); ) 732 { 733 Attribute entryAttribute = attributeIterator.next(); 734 735 attributes.put( toJndiAttribute( entryAttribute ) ); 736 } 737 738 return attributes; 739 } 740 741 return null; 742 } 743 744 745 /** 746 * Converts an {@link Attribute} to a JNDI Attribute. 747 * 748 * @param attribute the {@link Attribute} to convert 749 * @return the equivalent JNDI Attribute 750 */ 751 public static javax.naming.directory.Attribute toJndiAttribute( Attribute attribute ) 752 { 753 if ( attribute != null ) 754 { 755 javax.naming.directory.Attribute jndiAttribute = new BasicAttribute( attribute.getUpId() ); 756 757 // Looping on values 758 for ( Iterator<Value> valueIterator = attribute.iterator(); valueIterator.hasNext(); ) 759 { 760 Value value = valueIterator.next(); 761 762 if ( value.isHumanReadable() ) 763 { 764 jndiAttribute.add( value.getString() ); 765 } 766 else 767 { 768 jndiAttribute.add( value.getBytes() ); 769 } 770 } 771 772 return jndiAttribute; 773 } 774 775 return null; 776 } 777 778 779 /** 780 * Convert a JNDI Attribute to an LDAP API Attribute 781 * 782 * @param jndiAttribute the JNDI Attribute instance to convert 783 * @return An instance of a LDAP API Attribute object 784 * @throws LdapInvalidAttributeValueException If the attribute is invalid 785 */ 786 public static Attribute toApiAttribute( javax.naming.directory.Attribute jndiAttribute ) 787 throws LdapInvalidAttributeValueException 788 { 789 if ( jndiAttribute == null ) 790 { 791 return null; 792 } 793 794 try 795 { 796 Attribute attribute = new DefaultAttribute( jndiAttribute.getID() ); 797 798 for ( NamingEnumeration<?> values = jndiAttribute.getAll(); values.hasMoreElements(); ) 799 { 800 Object value = values.nextElement(); 801 802 if ( value instanceof String ) 803 { 804 attribute.add( ( String ) value ); 805 } 806 else if ( value instanceof byte[] ) 807 { 808 attribute.add( ( byte[] ) value ); 809 } 810 else 811 { 812 attribute.add( ( String ) null ); 813 } 814 } 815 816 return attribute; 817 } 818 catch ( NamingException ne ) 819 { 820 return null; 821 } 822 } 823}