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   *    http://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 static final SimpleDateFormat SDF = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT );
84  
85      private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
86  
87      // Initialize the dateFormat with the UTC TZ
88      static
89      {
90          SDF.setTimeZone( UTC_TIME_ZONE );
91      }
92  
93      /** Padding used to format number with a fixed size */
94      private static final String[] PADDING_6 = new String[]
95          { "00000", "0000", "000", "00", "0", "" };
96  
97      /** Padding used to format number with a fixed size */
98      private static final String[] PADDING_3 = new String[]
99          { "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      * &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replica ID&gt; # &lt;operation number&gt;
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 }