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 * https://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.schema; 021 022 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.UUID; 028 029import org.apache.directory.api.i18n.I18n; 030import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants; 031import org.apache.directory.api.ldap.model.constants.SchemaConstants; 032import org.apache.directory.api.ldap.model.entry.Attribute; 033import org.apache.directory.api.ldap.model.entry.Entry; 034import org.apache.directory.api.ldap.model.entry.Modification; 035import org.apache.directory.api.ldap.model.entry.Value; 036import org.apache.directory.api.ldap.model.exception.LdapException; 037import org.apache.directory.api.util.Strings; 038 039 040/** 041 * Various utility methods for schema functions and objects. 042 * 043 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 044 */ 045public final class SchemaUtils 046{ 047 /** 048 * Private constructor. 049 */ 050 private SchemaUtils() 051 { 052 } 053 054 055 /** 056 * Gets the target entry as it would look after a modification operation 057 * were performed on it. 058 * 059 * @param mods the modifications performed on the entry 060 * @param entry the source entry that is modified 061 * @return the resultant entry after the modifications have taken place 062 * @throws LdapException if there are problems accessing attributes 063 */ 064 public static Entry getTargetEntry( List<? extends Modification> mods, Entry entry ) 065 throws LdapException 066 { 067 Entry targetEntry = entry.clone(); 068 069 for ( Modification mod : mods ) 070 { 071 String id = mod.getAttribute().getId(); 072 073 switch ( mod.getOperation() ) 074 { 075 case REPLACE_ATTRIBUTE: 076 targetEntry.put( mod.getAttribute() ); 077 break; 078 079 case ADD_ATTRIBUTE: 080 Attribute combined = mod.getAttribute().clone(); 081 Attribute toBeAdded = mod.getAttribute(); 082 Attribute existing = entry.get( id ); 083 084 if ( existing != null ) 085 { 086 for ( Value value : existing ) 087 { 088 combined.add( value ); 089 } 090 } 091 092 for ( Value value : toBeAdded ) 093 { 094 combined.add( value ); 095 } 096 097 targetEntry.put( combined ); 098 break; 099 100 case REMOVE_ATTRIBUTE: 101 Attribute toBeRemoved = mod.getAttribute(); 102 103 if ( toBeRemoved.size() == 0 ) 104 { 105 targetEntry.removeAttributes( id ); 106 } 107 else 108 { 109 existing = targetEntry.get( id ); 110 111 if ( existing != null ) 112 { 113 for ( Value value : toBeRemoved ) 114 { 115 existing.remove( value ); 116 } 117 } 118 } 119 120 break; 121 122 case INCREMENT_ATTRIBUTE: 123 // The incremented attribute might not exist 124 AttributeType attributeType = mod.getAttribute().getAttributeType(); 125 String incrementStr = mod.getAttribute().getString(); 126 int increment = 1; 127 128 if ( !Strings.isEmpty( incrementStr ) ) 129 { 130 try 131 { 132 increment = Integer.parseInt( incrementStr ); 133 } 134 catch ( NumberFormatException nfe ) 135 { 136 throw new IllegalArgumentException( I18n.err( I18n.ERR_13866_MOD_INCREMENT_INVALID_VALUE, 137 attributeType.getName(), incrementStr ) ); 138 } 139 } 140 Attribute modified = targetEntry.get( attributeType ); 141 142 if ( !targetEntry.containsAttribute( attributeType ) ) 143 { 144 throw new IllegalArgumentException( I18n.err( I18n.ERR_13867_MOD_INCREMENT_NO_ATTRIBUTE, 145 attributeType.getName() ) ); 146 } 147 148 if ( !SchemaConstants.INTEGER_SYNTAX.equals( modified.getAttributeType().getSyntax().getOid() ) ) 149 { 150 throw new IllegalArgumentException( I18n.err( I18n.ERR_13868_MOD_INCREMENT_NO_INT_ATTRIBUTE, 151 attributeType.getName() ) ); 152 } 153 else 154 { 155 Value[] newValues = new Value[ modified.size() ]; 156 int i = 0; 157 158 for ( Value value : modified ) 159 { 160 int intValue = Integer.parseInt( value.getNormalized() ); 161 162 if ( intValue == Integer.MAX_VALUE ) 163 { 164 throw new IllegalArgumentException( I18n.err( I18n.ERR_13869_MOD_INCREMENT_OVERFLOW, 165 attributeType.getName(), intValue ) ); 166 } 167 168 newValues[i++] = new Value( Integer.toString( intValue + increment ) ); 169 modified.remove( value ); 170 } 171 172 modified.add( newValues ); 173 } 174 175 break; 176 177 default: 178 throw new IllegalStateException( I18n.err( I18n.ERR_13775_UNDEFINED_MODIFICATION_TYPE, mod.getOperation() ) ); 179 } 180 } 181 182 return targetEntry; 183 } 184 185 186 // ------------------------------------------------------------------------ 187 // qdescrs rendering operations 188 // ------------------------------------------------------------------------ 189 190 /** 191 * Renders qdescrs into an existing buffer. 192 * 193 * @param buf the string buffer to render the quoted description strs into 194 * @param qdescrs the quoted description strings to render 195 * @return the same string buffer that was given for call chaining 196 */ 197 public static StringBuilder render( StringBuilder buf, List<String> qdescrs ) 198 { 199 if ( ( qdescrs == null ) || qdescrs.isEmpty() ) 200 { 201 return buf; 202 } 203 else if ( qdescrs.size() == 1 ) 204 { 205 buf.append( "'" ).append( qdescrs.get( 0 ) ).append( "'" ); 206 } 207 else 208 { 209 buf.append( "( " ); 210 211 for ( String qdescr : qdescrs ) 212 { 213 buf.append( "'" ).append( qdescr ).append( "' " ); 214 } 215 216 buf.append( ")" ); 217 } 218 219 return buf; 220 } 221 222 223 /** 224 * Renders qdescrs into a new buffer.<br> 225 * <pre> 226 * descrs ::= qdescr | '(' WSP qdescrlist WSP ')' 227 * qdescrlist ::= [ qdescr ( SP qdescr )* ] 228 * qdescr ::= SQUOTE descr SQUOTE 229 * </pre> 230 * 231 * @param buf the string buffer to render the quoted description strings into 232 * @param qdescrs the quoted description strings to render 233 * @return the string buffer the qdescrs are rendered into 234 */ 235 /* No qualifier */static StringBuilder renderQDescrs( StringBuilder buf, List<String> qdescrs ) 236 { 237 if ( ( qdescrs == null ) || qdescrs.isEmpty() ) 238 { 239 return buf; 240 } 241 242 if ( qdescrs.size() == 1 ) 243 { 244 buf.append( '\'' ).append( qdescrs.get( 0 ) ).append( '\'' ); 245 } 246 else 247 { 248 buf.append( "( " ); 249 250 for ( String qdescr : qdescrs ) 251 { 252 buf.append( '\'' ).append( qdescr ).append( "' " ); 253 } 254 255 buf.append( ")" ); 256 } 257 258 return buf; 259 } 260 261 262 /** 263 * Renders QDString into a new buffer.<br> 264 * 265 * @param buf the string buffer to render the quoted description string into 266 * @param qdString the quoted description strings to render 267 * @return the string buffer the qdescrs are rendered into 268 */ 269 private static StringBuilder renderQDString( StringBuilder buf, String qdString ) 270 { 271 buf.append( '\'' ); 272 273 for ( char c : qdString.toCharArray() ) 274 { 275 switch ( c ) 276 { 277 case 0x27: 278 buf.append( "\\27" ); 279 break; 280 281 case 0x5C: 282 buf.append( "\\5C" ); 283 break; 284 285 default: 286 buf.append( c ); 287 break; 288 } 289 } 290 291 buf.append( '\'' ); 292 293 return buf; 294 } 295 296 297 // ------------------------------------------------------------------------ 298 // objectClass list rendering operations 299 // ------------------------------------------------------------------------ 300 301 /** 302 * Renders a list of object classes for things like a list of superior 303 * objectClasses using the ( oid $ oid ) format. 304 * 305 * @param ocs 306 * the objectClasses to list 307 * @return a buffer which contains the rendered list 308 */ 309 public static StringBuilder render( ObjectClass[] ocs ) 310 { 311 StringBuilder buf = new StringBuilder(); 312 313 return render( buf, ocs ); 314 } 315 316 317 /** 318 * Renders a list of object classes for things like a list of superior 319 * objectClasses using the ( oid $ oid ) format into an existing buffer. 320 * 321 * @param buf 322 * the string buffer to render the list of objectClasses into 323 * @param ocs 324 * the objectClasses to list 325 * @return a buffer which contains the rendered list 326 */ 327 public static StringBuilder render( StringBuilder buf, ObjectClass[] ocs ) 328 { 329 if ( ocs == null || ocs.length == 0 ) 330 { 331 return buf; 332 } 333 else if ( ocs.length == 1 ) 334 { 335 buf.append( ocs[0].getName() ); 336 } 337 else 338 { 339 buf.append( "( " ); 340 341 for ( int ii = 0; ii < ocs.length; ii++ ) 342 { 343 if ( ii + 1 < ocs.length ) 344 { 345 buf.append( ocs[ii].getName() ).append( " $ " ); 346 } 347 else 348 { 349 buf.append( ocs[ii].getName() ); 350 } 351 } 352 353 buf.append( " )" ); 354 } 355 356 return buf; 357 } 358 359 360 // ------------------------------------------------------------------------ 361 // attributeType list rendering operations 362 // ------------------------------------------------------------------------ 363 364 /** 365 * Renders a list of attributeTypes for things like the must or may list of 366 * objectClasses using the ( oid $ oid ) format. 367 * 368 * @param ats 369 * the attributeTypes to list 370 * @return a buffer which contains the rendered list 371 */ 372 public static StringBuilder render( AttributeType[] ats ) 373 { 374 StringBuilder buf = new StringBuilder(); 375 return render( buf, ats ); 376 } 377 378 379 /** 380 * Renders a list of attributeTypes for things like the must or may list of 381 * objectClasses using the ( oid $ oid ) format into an existing buffer. 382 * 383 * @param buf 384 * the string buffer to render the list of attributeTypes into 385 * @param ats 386 * the attributeTypes to list 387 * @return a buffer which contains the rendered list 388 */ 389 public static StringBuilder render( StringBuilder buf, AttributeType[] ats ) 390 { 391 if ( ats == null || ats.length == 0 ) 392 { 393 return buf; 394 } 395 else if ( ats.length == 1 ) 396 { 397 buf.append( ats[0].getName() ); 398 } 399 else 400 { 401 buf.append( "( " ); 402 for ( int ii = 0; ii < ats.length; ii++ ) 403 { 404 if ( ii + 1 < ats.length ) 405 { 406 buf.append( ats[ii].getName() ).append( " $ " ); 407 } 408 else 409 { 410 buf.append( ats[ii].getName() ); 411 } 412 } 413 buf.append( " )" ); 414 } 415 416 return buf; 417 } 418 419 420 // ------------------------------------------------------------------------ 421 // schema object rendering operations 422 // ------------------------------------------------------------------------ 423 424 /** 425 * Renders the schema extensions into a new StringBuffer. 426 * 427 * @param extensions the schema extensions map with key and values 428 * @return a StringBuffer with the extensions component of a syntax description 429 */ 430 public static StringBuilder render( Map<String, List<String>> extensions ) 431 { 432 StringBuilder buf = new StringBuilder(); 433 434 if ( extensions.isEmpty() ) 435 { 436 return buf; 437 } 438 439 for ( Map.Entry<String, List<String>> entry : extensions.entrySet() ) 440 { 441 buf.append( " " ).append( entry.getKey() ).append( " " ); 442 443 List<String> values = entry.getValue(); 444 445 // For extensions without values like X-IS-HUMAN-READIBLE 446 if ( values == null || values.isEmpty() ) 447 { 448 continue; 449 } 450 451 // For extensions with a single value we can use one qdstring like 'value' 452 if ( values.size() == 1 ) 453 { 454 buf.append( "'" ).append( values.get( 0 ) ).append( "' " ); 455 continue; 456 } 457 458 // For extensions with several values we have to surround whitespace 459 // separated list of qdstrings like ( 'value0' 'value1' 'value2' ) 460 buf.append( "( " ); 461 for ( String value : values ) 462 { 463 buf.append( "'" ).append( value ).append( "' " ); 464 } 465 buf.append( ")" ); 466 } 467 468 if ( buf.charAt( buf.length() - 1 ) != ' ' ) 469 { 470 buf.append( " " ); 471 } 472 473 return buf; 474 } 475 476 477 /** 478 * Returns a String description of a schema. The resulting String format is : 479 * <br> 480 * (OID [DESC '<description>'] FQCN <fcqn> [BYTECODE <bytecode>] X-SCHEMA '<schema>') 481 * <br> 482 * @param description The description to transform to a String 483 * @return The rendered schema object 484 */ 485 public static String render( LoadableSchemaObject description ) 486 { 487 StringBuilder buf = new StringBuilder(); 488 buf.append( "( " ).append( description.getOid() ); 489 490 if ( description.getDescription() != null ) 491 { 492 buf.append( " DESC " ); 493 renderQDString( buf, description.getDescription() ); 494 } 495 496 buf.append( " FQCN " ).append( description.getFqcn() ); 497 498 if ( !Strings.isEmpty( description.getBytecode() ) ) 499 { 500 buf.append( " BYTECODE " ).append( description.getBytecode() ); 501 } 502 503 buf.append( " X-SCHEMA '" ); 504 buf.append( getSchemaName( description ) ); 505 buf.append( "' )" ); 506 507 return buf.toString(); 508 } 509 510 511 private static String getSchemaName( SchemaObject desc ) 512 { 513 List<String> values = desc.getExtension( MetaSchemaConstants.X_SCHEMA_AT ); 514 515 if ( values == null || values.isEmpty() ) 516 { 517 return MetaSchemaConstants.SCHEMA_OTHER; 518 } 519 520 return values.get( 0 ); 521 } 522 523 524 /** 525 * Remove the options from the attributeType, and returns the ID. 526 * <br> 527 * RFC 4512 : 528 * <pre> 529 * attributedescription = attributetype options 530 * attributetype = oid 531 * options = *( SEMI option ) 532 * option = 1*keychar 533 * </pre> 534 * 535 * @param attributeId The AttributeType to parse 536 * @return The AttributeType without its options 537 */ 538 public static String stripOptions( String attributeId ) 539 { 540 int optionsPos = attributeId.indexOf( ';' ); 541 542 if ( optionsPos != -1 ) 543 { 544 return attributeId.substring( 0, optionsPos ); 545 } 546 else 547 { 548 return attributeId; 549 } 550 } 551 552 553 /** 554 * Get the options from the attributeType. 555 * <br> 556 * For instance, given : 557 * jpegphoto;binary;lang=jp 558 * <br> 559 * your get back a set containing { "binary", "lang=jp" } 560 * 561 * @param attributeId The AttributeType to parse 562 * @return a Set of options found for this AttributeType, or null 563 */ 564 public static Set<String> getOptions( String attributeId ) 565 { 566 int optionsPos = attributeId.indexOf( ';' ); 567 568 if ( optionsPos != -1 ) 569 { 570 Set<String> options = new HashSet<>(); 571 572 String[] res = attributeId.substring( optionsPos + 1 ).split( ";" ); 573 574 for ( String option : res ) 575 { 576 if ( !Strings.isEmpty( option ) ) 577 { 578 options.add( option ); 579 } 580 } 581 582 return options; 583 } 584 else 585 { 586 return null; 587 } 588 } 589 590 591 /** 592 * Transform an UUID in a byte array 593 * @param uuid The UUID to transform 594 * @return The byte[] representing the UUID 595 */ 596 public static byte[] uuidToBytes( UUID uuid ) 597 { 598 Long low = uuid.getLeastSignificantBits(); 599 Long high = uuid.getMostSignificantBits(); 600 byte[] bytes = new byte[16]; 601 602 bytes[0] = ( byte ) ( ( high & 0xff00000000000000L ) >> 56 ); 603 bytes[1] = ( byte ) ( ( high & 0x00ff000000000000L ) >> 48 ); 604 bytes[2] = ( byte ) ( ( high & 0x0000ff0000000000L ) >> 40 ); 605 bytes[3] = ( byte ) ( ( high & 0x000000ff00000000L ) >> 32 ); 606 bytes[4] = ( byte ) ( ( high & 0x00000000ff000000L ) >> 24 ); 607 bytes[5] = ( byte ) ( ( high & 0x0000000000ff0000L ) >> 16 ); 608 bytes[6] = ( byte ) ( ( high & 0x000000000000ff00L ) >> 8 ); 609 bytes[7] = ( byte ) ( high & 0x00000000000000ffL ); 610 bytes[8] = ( byte ) ( ( low & 0xff00000000000000L ) >> 56 ); 611 bytes[9] = ( byte ) ( ( low & 0x00ff000000000000L ) >> 48 ); 612 bytes[10] = ( byte ) ( ( low & 0x0000ff0000000000L ) >> 40 ); 613 bytes[11] = ( byte ) ( ( low & 0x000000ff00000000L ) >> 32 ); 614 bytes[12] = ( byte ) ( ( low & 0x00000000ff000000L ) >> 24 ); 615 bytes[13] = ( byte ) ( ( low & 0x0000000000ff0000L ) >> 16 ); 616 bytes[14] = ( byte ) ( ( low & 0x000000000000ff00L ) >> 8 ); 617 bytes[15] = ( byte ) ( low & 0x00000000000000ffL ); 618 619 return bytes; 620 } 621 622 623 /** 624 * Tells if an AttributeType name is valid or not. An Attribute name is valid if 625 * it's a descr / numericoid, as described in rfc4512 : 626 * <pre> 627 * name = descr / numericOid 628 * descr = keystring 629 * keystring = leadkeychar *keychar 630 * leadkeychar = ALPHA 631 * keychar = ALPHA / DIGIT / HYPHEN / USCORE 632 * numericoid = number 1*( DOT number ) 633 * number = DIGIT / ( LDIGIT 1*DIGIT ) 634 * ALPHA = %x41-5A / %x61-7A ; "A"-"Z" / "a"-"z" 635 * DIGIT = %x30 / LDIGIT ; "0"-"9" 636 * HYPHEN = %x2D ; hyphen ("-") 637 * LDIGIT = %x31-39 ; "1"-"9" 638 * DOT = %x2E ; period (".") 639 * USCORE = %x5F ; underscore ("_") 640 * </pre> 641 * 642 * Note that we have extended this grammar to accept the '_' char, which is widely used in teh LDAP world. 643 * 644 * @param attributeName The AttributeType name to check 645 * @return true if it's valid 646 */ 647 public static boolean isAttributeNameValid( String attributeName ) 648 { 649 if ( Strings.isEmpty( attributeName ) ) 650 { 651 return false; 652 } 653 654 // Check the first char which must be ALPHA or DIGIT 655 boolean descr; 656 boolean zero = false; 657 boolean dot = false; 658 659 char c = attributeName.charAt( 0 ); 660 661 if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) ) ) 662 { 663 descr = true; 664 } 665 else if ( ( c >= '0' ) && ( c <= '9' ) ) 666 { 667 descr = false; 668 669 zero = c == '0'; 670 } 671 else 672 { 673 return false; 674 } 675 676 for ( int i = 1; i < attributeName.length(); i++ ) 677 { 678 c = attributeName.charAt( i ); 679 680 if ( descr ) 681 { 682 // This is a descr, iterate on KeyChars (ALPHA / DIGIT / HYPHEN / USCORE) 683 if ( ( ( c < 'a' ) || ( c > 'z' ) ) 684 && ( ( c < 'A' ) || ( c > 'Z' ) ) 685 && ( ( c < '0' ) || ( c > '9' ) ) 686 && ( c != '-' ) 687 && ( c != '_' ) ) 688 { 689 return false; 690 } 691 } 692 else 693 { 694 // This is a numericOid, check it 695 if ( c == '.' ) 696 { 697 // Not allowed if we already have had a dot 698 if ( dot ) 699 { 700 return false; 701 } 702 703 dot = true; 704 zero = false; 705 } 706 else if ( ( c >= '0' ) && ( c <= '9' ) ) 707 { 708 dot = false; 709 710 if ( zero ) 711 { 712 // We can't have a leading '0' followed by another number 713 return false; 714 } 715 else if ( c == '0' ) 716 { 717 zero = true; 718 } 719 } 720 else 721 { 722 // Not valid 723 return false; 724 } 725 } 726 } 727 728 return true; 729 } 730}