View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    https://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.api.ldap.model.csn;
21  
22  
23  import java.text.ParseException;
24  import java.text.SimpleDateFormat;
25  import java.util.Date;
26  import java.util.Locale;
27  import java.util.TimeZone;
28  
29  import org.apache.directory.api.i18n.I18n;
30  import org.apache.directory.api.util.Chars;
31  import org.apache.directory.api.util.Strings;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  
36  /**
37   * Represents 'Change Sequence Number' in LDUP specification.
38   * 
39   * A CSN is a composition of a timestamp, a replica ID and a 
40   * operation sequence number.
41   * 
42   * It's described in http://tools.ietf.org/html/draft-ietf-ldup-model-09.
43   * 
44   * The CSN syntax is :
45   * <pre>
46   * &lt;CSN&gt;            ::= &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replicaId&gt; # &lt;modifierNumber&gt;
47   * &lt;timestamp&gt;      ::= A GMT based time, YYYYmmddHHMMSS.uuuuuuZ
48   * &lt;changeCount&gt;    ::= [000000-ffffff] 
49   * &lt;replicaId&gt;      ::= [000-fff]
50   * &lt;modifierNumber&gt; ::= [000000-ffffff]
51   * </pre>
52   *  
53   * It distinguishes a change made on an object on a server,
54   * and if two operations take place during the same timeStamp,
55   * the operation sequence number makes those operations distinct.
56   * 
57   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
58   */
59  public class Csn implements Comparable<Csn>
60  {
61      /** The logger for this class */
62      private static final Logger LOG = LoggerFactory.getLogger( Csn.class );
63  
64      /** The timeStamp of this operation */
65      private final long timestamp;
66  
67      /** The server identification */
68      private final int replicaId;
69  
70      /** The operation number in a modification operation */
71      private final int operationNumber;
72  
73      /** The changeCount to distinguish operations done in the same second */
74      private final int changeCount;
75  
76      /** Stores the String representation of the CSN */
77      private String csnStr;
78  
79      /** Stores the byte array representation of the CSN */
80      private byte[] bytes;
81  
82      /** The Timestamp syntax. The last 'z' is _not_ the Time Zone */
83      private final SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT );
84  
85      private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
86  
87      /** Padding used to format number with a fixed size */
88      private static final String[] PADDING_6 = new String[]
89          { "00000", "0000", "000", "00", "0", "" };
90  
91      /** Padding used to format number with a fixed size */
92      private static final String[] PADDING_3 = new String[]
93          { "00", "0", "" };
94  
95  
96      /**
97       * Creates a new instance.
98       * <b>This method should be used only for deserializing a CSN</b> 
99       * 
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      * &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replica ID&gt; # &lt;operation number&gt;
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 }