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