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