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