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.csn; 021 022 023import java.text.ParseException; 024import java.text.SimpleDateFormat; 025import java.util.Date; 026import java.util.Locale; 027import java.util.TimeZone; 028 029import org.apache.directory.api.i18n.I18n; 030import org.apache.directory.api.util.Chars; 031import org.apache.directory.api.util.Strings; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035 036/** 037 * Represents 'Change Sequence Number' in LDUP specification. 038 * 039 * A CSN is a composition of a timestamp, a replica ID and a 040 * operation sequence number. 041 * 042 * It's described in http://tools.ietf.org/html/draft-ietf-ldup-model-09. 043 * 044 * The CSN syntax is : 045 * <pre> 046 * <CSN> ::= <timestamp> # <changeCount> # <replicaId> # <modifierNumber> 047 * <timestamp> ::= A GMT based time, YYYYmmddHHMMSS.uuuuuuZ 048 * <changeCount> ::= [000000-ffffff] 049 * <replicaId> ::= [000-fff] 050 * <modifierNumber> ::= [000000-ffffff] 051 * </pre> 052 * 053 * It distinguishes a change made on an object on a server, 054 * and if two operations take place during the same timeStamp, 055 * the operation sequence number makes those operations distinct. 056 * 057 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 058 */ 059public class Csn implements Comparable<Csn> 060{ 061 /** The logger for this class */ 062 private static final Logger LOG = LoggerFactory.getLogger( Csn.class ); 063 064 /** The timeStamp of this operation */ 065 private final long timestamp; 066 067 /** The server identification */ 068 private final int replicaId; 069 070 /** The operation number in a modification operation */ 071 private final int operationNumber; 072 073 /** The changeCount to distinguish operations done in the same second */ 074 private final int changeCount; 075 076 /** Stores the String representation of the CSN */ 077 private String csnStr; 078 079 /** Stores the byte array representation of the CSN */ 080 private byte[] bytes; 081 082 /** The Timestamp syntax. The last 'z' is _not_ the Time Zone */ 083 private final SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT ); 084 085 private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" ); 086 087 /** Padding used to format number with a fixed size */ 088 private static final String[] PADDING_6 = new String[] 089 { "00000", "0000", "000", "00", "0", "" }; 090 091 /** Padding used to format number with a fixed size */ 092 private static final String[] PADDING_3 = new String[] 093 { "00", "0", "" }; 094 095 096 /** 097 * Creates a new instance. 098 * <b>This method should be used only for deserializing a CSN</b> 099 * 100 * @param timestamp GMT timestamp of modification 101 * @param changeCount The operation increment 102 * @param replicaId Replica ID where modification occurred (<tt>[-_A-Za-z0-9]{1,16}</tt>) 103 * @param operationNumber Operation number in a modification operation 104 */ 105 public Csn( long timestamp, int changeCount, int replicaId, int operationNumber ) 106 { 107 this.timestamp = timestamp; 108 this.replicaId = replicaId; 109 this.operationNumber = operationNumber; 110 this.changeCount = changeCount; 111 sdf.setTimeZone( UTC_TIME_ZONE ); 112 } 113 114 115 /** 116 * Creates a new instance of SimpleCSN from a String. 117 * 118 * The string format must be : 119 * <timestamp> # <changeCount> # <replica ID> # <operation number> 120 * 121 * @param value The String containing the CSN 122 */ 123 public Csn( String value ) 124 { 125 sdf.setTimeZone( UTC_TIME_ZONE ); 126 127 if ( Strings.isEmpty( value ) ) 128 { 129 String message = I18n.err( I18n.ERR_13015_NULL_OR_EMPTY_CSN ); 130 LOG.error( message ); 131 throw new InvalidCSNException( message ); 132 } 133 134 if ( value.length() != 40 ) 135 { 136 String message = I18n.err( I18n.ERR_13016_INCORRECT_CSN_LENGTH ); 137 LOG.error( message ); 138 throw new InvalidCSNException( message ); 139 } 140 141 // Get the Timestamp 142 int sepTS = value.indexOf( '#' ); 143 144 if ( sepTS < 0 ) 145 { 146 String message = I18n.err( I18n.ERR_13017_CANT_FIND_SHARP_IN_CSN ); 147 LOG.error( message ); 148 throw new InvalidCSNException( message ); 149 } 150 151 String timestampStr = value.substring( 0, sepTS ).trim(); 152 153 if ( timestampStr.length() != 22 ) 154 { 155 String message = I18n.err( I18n.ERR_13018_TIMESTAMP_NOT_LONG_ENOUGH ); 156 LOG.error( message ); 157 throw new InvalidCSNException( message ); 158 } 159 160 // Let's transform the Timestamp by removing the mulliseconds and microseconds 161 String realTimestamp = timestampStr.substring( 0, 14 ); 162 163 long tempTimestamp = 0L; 164 165 synchronized ( sdf ) 166 { 167 try 168 { 169 tempTimestamp = sdf.parse( realTimestamp ).getTime(); 170 } 171 catch ( ParseException pe ) 172 { 173 String message = I18n.err( I18n.ERR_13019_CANNOT_PARSE_TIMESTAMP, timestampStr ); 174 LOG.error( message ); 175 throw new InvalidCSNException( message, pe ); 176 } 177 } 178 179 int millis = 0; 180 181 // And add the milliseconds and microseconds now 182 try 183 { 184 millis = Integer.parseInt( timestampStr.substring( 15, 21 ) ); 185 } 186 catch ( NumberFormatException nfe ) 187 { 188 String message = I18n.err( I18n.ERR_13020_INVALID_MICROSECOND ); 189 LOG.error( message ); 190 throw new InvalidCSNException( message, nfe ); 191 } 192 193 tempTimestamp += ( millis / 1000 ); 194 timestamp = tempTimestamp; 195 196 // Get the changeCount. It should be an hex number prefixed with '0x' 197 int sepCC = value.indexOf( '#', sepTS + 1 ); 198 199 if ( sepCC < 0 ) 200 { 201 String message = I18n.err( I18n.ERR_13014_DN_ATTR_FLAG_INVALID, value ); 202 LOG.error( message ); 203 throw new InvalidCSNException( message ); 204 } 205 206 String changeCountStr = value.substring( sepTS + 1, sepCC ).trim(); 207 208 try 209 { 210 changeCount = Integer.parseInt( changeCountStr, 16 ); 211 } 212 catch ( NumberFormatException nfe ) 213 { 214 String message = I18n.err( I18n.ERR_13021_INVALID_CHANGE_COUNT, changeCountStr ); 215 LOG.error( message ); 216 throw new InvalidCSNException( message, nfe ); 217 } 218 219 // Get the replicaID 220 int sepRI = value.indexOf( '#', sepCC + 1 ); 221 222 if ( sepRI < 0 ) 223 { 224 String message = I18n.err( I18n.ERR_13022_MISSING_SHARP_IN_CSN, value ); 225 LOG.error( message ); 226 throw new InvalidCSNException( message ); 227 } 228 229 String replicaIdStr = value.substring( sepCC + 1, sepRI ).trim(); 230 231 if ( Strings.isEmpty( replicaIdStr ) ) 232 { 233 String message = I18n.err( I18n.ERR_13023_REPLICA_ID_NULL ); 234 LOG.error( message ); 235 throw new InvalidCSNException( message ); 236 } 237 238 try 239 { 240 replicaId = Integer.parseInt( replicaIdStr, 16 ); 241 } 242 catch ( NumberFormatException nfe ) 243 { 244 String message = I18n.err( I18n.ERR_13024_INVALID_REPLICA_ID, replicaIdStr ); 245 LOG.error( message ); 246 throw new InvalidCSNException( message, nfe ); 247 } 248 249 // Get the modification number 250 if ( sepCC == value.length() ) 251 { 252 String message = I18n.err( I18n.ERR_13025_NO_OPERATION_NUMBER ); 253 LOG.error( message ); 254 throw new InvalidCSNException( message ); 255 } 256 257 String operationNumberStr = value.substring( sepRI + 1 ).trim(); 258 259 try 260 { 261 operationNumber = Integer.parseInt( operationNumberStr, 16 ); 262 } 263 catch ( NumberFormatException nfe ) 264 { 265 String message = I18n.err( I18n.ERR_13026_INVALID_OPERATION_NUMBER, operationNumberStr ); 266 LOG.error( message ); 267 throw new InvalidCSNException( message, nfe ); 268 } 269 270 csnStr = value; 271 bytes = Strings.getBytesUtf8( csnStr ); 272 } 273 274 275 /** 276 * Creates a new instance of SimpleCSN from the serialized data 277 * 278 * @param value The byte array which contains the serialized CSN 279 */ 280 Csn( byte[] value ) 281 { 282 csnStr = Strings.utf8ToString( value ); 283 Csn csn = new Csn( csnStr ); 284 timestamp = csn.timestamp; 285 changeCount = csn.changeCount; 286 replicaId = csn.replicaId; 287 operationNumber = csn.operationNumber; 288 bytes = Strings.getBytesUtf8( csnStr ); 289 } 290 291 292 /** 293 * Check if the given String is a valid CSN. 294 * 295 * @param value The String to check 296 * @return <code>true</code> if the String is a valid CSN 297 */ 298 public static boolean isValid( String value ) 299 { 300 if ( Strings.isEmpty( value ) ) 301 { 302 return false; 303 } 304 305 char[] chars = value.toCharArray(); 306 307 if ( chars.length != 40 ) 308 { 309 return false; 310 } 311 312 // Get the Timestamp 313 // Check the timestamp's year 314 for ( int pos = 0; pos < 4; pos++ ) 315 { 316 if ( !Chars.isDigit( chars[pos] ) ) 317 { 318 return false; 319 } 320 } 321 322 // Check the timestamp month 323 switch ( chars[4] ) 324 { 325 case '0' : 326 if ( !Chars.isDigit( chars[5] ) ) 327 { 328 return false; 329 } 330 331 if ( chars[5] == '0' ) 332 { 333 return false; 334 } 335 336 break; 337 338 case '1' : 339 if ( ( chars[5] != '0' ) && ( chars[5] != '1' ) && ( chars[5] != '2' ) ) 340 { 341 return false; 342 } 343 344 break; 345 346 default : 347 return false; 348 } 349 350 // Check the timestamp day 351 switch ( chars[6] ) 352 { 353 case '0' : 354 if ( !Chars.isDigit( chars[7] ) ) 355 { 356 return false; 357 } 358 359 if ( chars[7] == '0' ) 360 { 361 return false; 362 } 363 364 break; 365 366 case '1' : 367 case '2' : // Special case for february... 368 if ( !Chars.isDigit( chars[7] ) ) 369 { 370 return false; 371 } 372 373 break; 374 375 case '3' : 376 // Deal with 30 days months 377 if ( ( chars[7] != '0' ) && ( chars[7] != '1' ) ) 378 { 379 return false; 380 } 381 382 break; 383 384 default : 385 return false; 386 } 387 388 // Check the timestamp hour 389 switch ( chars[8] ) 390 { 391 case '0' : 392 case '1' : 393 if ( !Chars.isDigit( chars[9] ) ) 394 { 395 return false; 396 } 397 398 break; 399 400 case '2' : 401 if ( ( chars[9] != '0' ) && ( chars[9] != '1' ) && ( chars[9] != '2' ) && ( chars[9] != '3' ) ) 402 { 403 return false; 404 } 405 406 break; 407 408 default : 409 return false; 410 } 411 412 // Check the timestamp minute 413 switch ( chars[10] ) 414 { 415 case '0' : 416 case '1' : 417 case '2' : 418 case '3' : 419 case '4' : 420 case '5' : 421 break; 422 423 default : 424 return false; 425 } 426 427 if ( !Chars.isDigit( chars[11] ) ) 428 { 429 return false; 430 } 431 432 // Check the timestamp seconds 433 switch ( chars[12] ) 434 { 435 case '0' : 436 case '1' : 437 case '2' : 438 case '3' : 439 case '4' : 440 case '5' : 441 break; 442 443 default : 444 return false; 445 } 446 447 if ( !Chars.isDigit( chars[13] ) ) 448 { 449 return false; 450 } 451 452 // Check the milliseconds 453 if ( chars[14] != '.' ) 454 { 455 return false; 456 } 457 458 for ( int i = 0; i < 6; i++ ) 459 { 460 if ( !Chars.isDigit( chars[15 + i] ) ) 461 { 462 return false; 463 } 464 } 465 466 if ( chars[21] != 'Z' ) 467 { 468 return false; 469 } 470 471 if ( chars[22] != '#' ) 472 { 473 return false; 474 } 475 476 // Get the changeCount. It should be an 6 digit hex number 477 if ( !Chars.isHex( ( byte ) chars[23] ) 478 || !Chars.isHex( ( byte ) chars[24] ) 479 || !Chars.isHex( ( byte ) chars[25] ) 480 || !Chars.isHex( ( byte ) chars[26] ) 481 || !Chars.isHex( ( byte ) chars[27] ) 482 || !Chars.isHex( ( byte ) chars[28] ) ) 483 { 484 return false; 485 } 486 487 if ( chars[29] != '#' ) 488 { 489 return false; 490 } 491 492 // Get the replicaID, which should be a 3 digits hex number 493 if ( !Chars.isHex( ( byte ) chars[30] ) 494 || !Chars.isHex( ( byte ) chars[31] ) 495 || !Chars.isHex( ( byte ) chars[32] ) ) 496 { 497 return false; 498 } 499 500 if ( chars[33] != '#' ) 501 { 502 return false; 503 } 504 505 // Check the modification number, which should be a 6 digits hex number 506 if ( !Chars.isHex( ( byte ) chars[34] ) 507 || !Chars.isHex( ( byte ) chars[35] ) 508 || !Chars.isHex( ( byte ) chars[36] ) 509 || !Chars.isHex( ( byte ) chars[37] ) 510 || !Chars.isHex( ( byte ) chars[38] ) 511 || !Chars.isHex( ( byte ) chars[39] ) ) 512 { 513 return false; 514 } 515 516 return true; 517 } 518 519 520 /** 521 * Get the CSN as a byte array. The data are stored as : 522 * bytes 1 to 8 : timestamp, big-endian 523 * bytes 9 to 12 : change count, big endian 524 * bytes 13 to ... : ReplicaId 525 * 526 * @return A copy of the byte array representing theCSN 527 */ 528 public byte[] getBytes() 529 { 530 if ( bytes == null ) 531 { 532 bytes = Strings.getBytesUtf8( csnStr ); 533 } 534 535 byte[] copy = new byte[bytes.length]; 536 System.arraycopy( bytes, 0, copy, 0, bytes.length ); 537 return copy; 538 } 539 540 541 /** 542 * @return The timestamp 543 */ 544 public long getTimestamp() 545 { 546 return timestamp; 547 } 548 549 550 /** 551 * @return The changeCount 552 */ 553 public int getChangeCount() 554 { 555 return changeCount; 556 } 557 558 559 /** 560 * @return The replicaId 561 */ 562 public int getReplicaId() 563 { 564 return replicaId; 565 } 566 567 568 /** 569 * @return The operation number 570 */ 571 public int getOperationNumber() 572 { 573 return operationNumber; 574 } 575 576 577 /** 578 * @return The CSN as a String 579 */ 580 @Override 581 public String toString() 582 { 583 if ( csnStr == null ) 584 { 585 StringBuilder buf = new StringBuilder( 40 ); 586 587 synchronized ( sdf ) 588 { 589 buf.append( sdf.format( new Date( timestamp ) ) ); 590 } 591 592 // Add the milliseconds part 593 long millis = ( timestamp % 1000 ) * 1000; 594 String millisStr = Long.toString( millis ); 595 596 buf.append( '.' ).append( PADDING_6[millisStr.length() - 1] ).append( millisStr ).append( "Z#" ); 597 598 String countStr = Integer.toHexString( changeCount ); 599 600 buf.append( PADDING_6[countStr.length() - 1] ).append( countStr ); 601 buf.append( '#' ); 602 603 String replicaIdStr = Integer.toHexString( replicaId ); 604 605 buf.append( PADDING_3[replicaIdStr.length() - 1] ).append( replicaIdStr ); 606 buf.append( '#' ); 607 608 String operationNumberStr = Integer.toHexString( operationNumber ); 609 610 buf.append( PADDING_6[operationNumberStr.length() - 1] ).append( operationNumberStr ); 611 612 csnStr = buf.toString(); 613 } 614 615 return csnStr; 616 } 617 618 619 /** 620 * Returns a hash code value for the object. 621 * 622 * @return a hash code value for this object. 623 */ 624 @Override 625 public int hashCode() 626 { 627 int h = 37; 628 629 h = h * 17 + ( int ) ( timestamp ^ ( timestamp >>> 32 ) ); 630 h = h * 17 + changeCount; 631 h = h * 17 + replicaId; 632 h = h * 17 + operationNumber; 633 634 return h; 635 } 636 637 638 /** 639 * Indicates whether some other object is "equal to" this one 640 * 641 * @param o the reference object with which to compare. 642 * @return <code>true</code> if this object is the same as the obj argument; 643 * <code>false</code> otherwise. 644 */ 645 @Override 646 public boolean equals( Object o ) 647 { 648 if ( this == o ) 649 { 650 return true; 651 } 652 653 if ( !( o instanceof Csn ) ) 654 { 655 return false; 656 } 657 658 Csn that = ( Csn ) o; 659 660 return ( timestamp == that.timestamp ) && ( changeCount == that.changeCount ) 661 && ( replicaId == that.replicaId ) && ( operationNumber == that.operationNumber ); 662 } 663 664 665 /** 666 * Compares this object with the specified object for order. Returns a 667 * negative integer, zero, or a positive integer as this object is less 668 * than, equal to, or greater than the specified object.<p> 669 * 670 * @param csn the Object to be compared. 671 * @return a negative integer, zero, or a positive integer as this object 672 * is less than, equal to, or greater than the specified object. 673 */ 674 @Override 675 public int compareTo( Csn csn ) 676 { 677 if ( csn == null ) 678 { 679 return 1; 680 } 681 682 // Compares the timestamp first 683 if ( this.timestamp < csn.timestamp ) 684 { 685 return -1; 686 } 687 else if ( this.timestamp > csn.timestamp ) 688 { 689 return 1; 690 } 691 692 // Then the change count 693 if ( this.changeCount < csn.changeCount ) 694 { 695 return -1; 696 } 697 else if ( this.changeCount > csn.changeCount ) 698 { 699 return 1; 700 } 701 702 // Then the replicaId 703 int replicaIdCompareResult = getReplicaIdCompareResult( csn ); 704 705 if ( replicaIdCompareResult != 0 ) 706 { 707 return replicaIdCompareResult; 708 } 709 710 // Last, not least, compares the operation number 711 if ( this.operationNumber < csn.operationNumber ) 712 { 713 return -1; 714 } 715 else if ( this.operationNumber > csn.operationNumber ) 716 { 717 return 1; 718 } 719 else 720 { 721 return 0; 722 } 723 } 724 725 726 private int getReplicaIdCompareResult( Csn csn ) 727 { 728 if ( this.replicaId < csn.replicaId ) 729 { 730 return -1; 731 } 732 if ( this.replicaId > csn.replicaId ) 733 { 734 return 1; 735 } 736 return 0; 737 } 738}