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.TimeZone;
27  
28  import org.apache.directory.api.i18n.I18n;
29  import org.apache.directory.api.util.Chars;
30  import org.apache.directory.api.util.Strings;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  
35  /**
36   * Represents 'Change Sequence Number' in LDUP specification.
37   * 
38   * A CSN is a composition of a timestamp, a replica ID and a 
39   * operation sequence number.
40   * 
41   * It's described in http://tools.ietf.org/html/draft-ietf-ldup-model-09.
42   * 
43   * The CSN syntax is :
44   * <pre>
45   * <CSN>            ::= <timestamp> # <changeCount> # <replicaId> # <modifierNumber>
46   * <timestamp>      ::= A GMT based time, YYYYmmddHHMMSS.uuuuuuZ
47   * <changeCount>    ::= [000000-ffffff] 
48   * <replicaId>      ::= [000-fff]
49   * <modifierNumber> ::= [000000-ffffff]
50   * </pre>
51   *  
52   * It distinguishes a change made on an object on a server,
53   * and if two operations take place during the same timeStamp,
54   * the operation sequence number makes those operations distinct.
55   * 
56   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
57   */
58  public class Csn implements Comparable<Csn>
59  {
60      /** The logger for this class */
61      private static final Logger LOG = LoggerFactory.getLogger( Csn.class );
62  
63      /** The timeStamp of this operation */
64      private final long timestamp;
65  
66      /** The server identification */
67      private final int replicaId;
68  
69      /** The operation number in a modification operation */
70      private final int operationNumber;
71  
72      /** The changeCount to distinguish operations done in the same second */
73      private final int changeCount;
74  
75      /** Stores the String representation of the CSN */
76      private String csnStr;
77  
78      /** Stores the byte array representation of the CSN */
79      private byte[] bytes;
80  
81      /** The Timestamp syntax. The last 'z' is _not_ the Time Zone */
82      private static final SimpleDateFormat SDF = new SimpleDateFormat( "yyyyMMddHHmmss" );
83  
84      private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
85  
86      // Initialize the dateFormat with the UTC TZ
87      static
88      {
89          SDF.setTimeZone( UTC_TIME_ZONE );
90      }
91  
92      /** Padding used to format number with a fixed size */
93      private static final String[] PADDING_6 = new String[]
94          { "00000", "0000", "000", "00", "0", "" };
95  
96      /** Padding used to format number with a fixed size */
97      private static final String[] PADDING_3 = new String[]
98          { "00", "0", "" };
99  
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      * &lt;timestamp> # &lt;changeCount> # &lt;replica ID> # &lt;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         char[] chars = value.toCharArray();
292         
293         if ( chars.length != 40 )
294         {
295             return false;
296         }
297 
298         // Get the Timestamp
299         // Check the timestamp's year
300         for ( int pos = 0; pos < 4; pos++ )
301         {
302             if ( !Chars.isDigit( chars[pos] ) )
303             {
304                 return false;
305             }
306         }
307         
308         // Check the timestamp month
309         switch ( chars[4] )
310         {
311             case '0' :
312                 if ( !Chars.isDigit( chars[5] ) )
313                 {
314                     return false;
315                 }
316                 
317                 if ( chars[5] == '0' )
318                 {
319                     return false;
320                 }
321                 
322                 break;
323                 
324             case '1' :
325                 if ( ( chars[5] != '0' ) && ( chars[5] != '1' ) && ( chars[5] != '2' ) )
326                 {
327                     return false;
328                 }
329                 
330                 break;
331                 
332             default :
333                 return false;
334         }
335 
336         // Check the timestamp day
337         switch ( chars[6] )
338         {
339             case '0' :
340                 if ( !Chars.isDigit( chars[7] ) )
341                 {
342                     return false;
343                 }
344                 
345                 if ( chars[7] == '0' )
346                 {
347                     return false;
348                 }
349                 
350                 break;
351                 
352             case '1' :
353                 if ( !Chars.isDigit( chars[7] ) )
354                 {
355                     return false;
356                 }
357                 
358                 break;
359                 
360             case '2' :
361                 if ( !Chars.isDigit( chars[7] ) )
362                 {
363                     return false;
364                 }
365                 
366                 // Special case for february...
367                 break;
368                 
369             case '3' :
370                 // Deal with 30 days months
371                 if ( ( chars[7] != '0' ) && ( chars[7] != '1' ) )
372                 {
373                     return false;
374                 }
375                 
376                 break;
377                 
378             default :
379                 return false;
380         }
381 
382         // Check the timestamp hour
383         switch ( chars[8] )
384         {
385             case '0' :
386             case '1' :
387                 if ( !Chars.isDigit( chars[9] ) )
388                 {
389                     return false;
390                 }
391 
392                 break;
393                 
394             case '2' :
395                 if ( ( chars[9] != '0' ) && ( chars[9] != '1' ) && ( chars[9] != '2' ) && ( chars[9] != '3' ) )
396                 {
397                     return false;
398                 }
399                 
400                 break;
401                 
402             default :
403                 return false;
404         }
405 
406         // Check the timestamp minute
407         switch ( chars[10] )
408         {
409             case '0' :
410             case '1' :
411             case '2' :
412             case '3' :
413             case '4' :
414             case '5' :
415                 break;
416                 
417             default :
418                 return false;
419         }
420         
421         if ( !Chars.isDigit( chars[11] ) )
422         {
423             return false;
424         }
425         
426         // Check the timestamp seconds
427         switch ( chars[12] )
428         {
429             case '0' :
430             case '1' :
431             case '2' :
432             case '3' :
433             case '4' :
434             case '5' :
435                 break;
436                 
437             default :
438                 return false;
439         }
440         
441         if ( !Chars.isDigit( chars[13] ) )
442         {
443             return false;
444         }
445 
446         // Check the milliseconds
447         if ( chars[14] != '.' )
448         {
449             return false;
450         }
451 
452         for ( int i = 0; i < 6; i++ )
453         {
454             if ( !Chars.isDigit( chars[15 + i] ) )
455             {
456                 return false;
457             }
458         }
459 
460         if ( chars[21] != 'Z' )
461         {
462             return false;
463         }
464 
465         if ( chars[22] != '#' )
466         {
467             return false;
468         }
469 
470         // Get the changeCount. It should be an 6 digit hex number
471         if ( !Chars.isHex( (byte)chars[23] ) ||
472              !Chars.isHex( (byte)chars[24] ) ||
473              !Chars.isHex( (byte)chars[25] ) ||
474              !Chars.isHex( (byte)chars[26] ) ||
475              !Chars.isHex( (byte)chars[27] ) ||
476              !Chars.isHex( (byte)chars[28] ) )
477         {
478             return false;
479         }
480 
481         if ( chars[29] != '#' )
482         {
483             return false;
484         }
485         
486         // Get the replicaID, which should be a 3 digits hex number
487         if ( !Chars.isHex( (byte)chars[30] ) || 
488              !Chars.isHex( (byte)chars[31] ) || 
489              !Chars.isHex( (byte)chars[32] ) )
490         {
491             return false;
492         }
493 
494         if ( chars[33] != '#' )
495         {
496             return false;
497         }
498 
499         // Check the modification number, which should be a 6 digits hex number
500         if ( !Chars.isHex( (byte)chars[34] ) ||
501              !Chars.isHex( (byte)chars[35] ) ||
502              !Chars.isHex( (byte)chars[36] ) ||
503              !Chars.isHex( (byte)chars[37] ) ||
504              !Chars.isHex( (byte)chars[38] ) ||
505              !Chars.isHex( (byte)chars[39] ) )
506         {
507             return false;
508         }
509 
510         return true;
511     }
512 
513 
514     /**
515      * Creates a new instance of SimpleCSN from the serialized data
516      *
517      * @param value The byte array which contains the serialized CSN
518      */
519     Csn( byte[] value )
520     {
521         csnStr = Strings.utf8ToString( value );
522         Csn csn = new Csn( csnStr );
523         timestamp = csn.timestamp;
524         changeCount = csn.changeCount;
525         replicaId = csn.replicaId;
526         operationNumber = csn.operationNumber;
527         bytes = Strings.getBytesUtf8( csnStr );
528     }
529 
530 
531     /**
532      * Get the CSN as a byte array. The data are stored as :
533      * bytes 1 to 8  : timestamp, big-endian
534      * bytes 9 to 12 : change count, big endian
535      * bytes 13 to ... : ReplicaId 
536      * 
537      * @return A copy of the byte array representing theCSN
538      */
539     public byte[] getBytes()
540     {
541         if ( bytes == null )
542         {
543             bytes = Strings.getBytesUtf8( csnStr );
544         }
545 
546         byte[] copy = new byte[bytes.length];
547         System.arraycopy( bytes, 0, copy, 0, bytes.length );
548         return copy;
549     }
550 
551 
552     /**
553      * @return The timestamp
554      */
555     public long getTimestamp()
556     {
557         return timestamp;
558     }
559 
560 
561     /**
562      * @return The changeCount
563      */
564     public int getChangeCount()
565     {
566         return changeCount;
567     }
568 
569 
570     /**
571      * @return The replicaId
572      */
573     public int getReplicaId()
574     {
575         return replicaId;
576     }
577 
578 
579     /**
580      * @return The operation number
581      */
582     public int getOperationNumber()
583     {
584         return operationNumber;
585     }
586 
587 
588     /**
589      * @return The CSN as a String
590      */
591     public String toString()
592     {
593         if ( csnStr == null )
594         {
595             StringBuilder buf = new StringBuilder( 40 );
596 
597             synchronized ( SDF )
598             {
599                 buf.append( SDF.format( new Date( timestamp ) ) );
600             }
601 
602             // Add the milliseconds part
603             long millis = ( timestamp % 1000 ) * 1000;
604             String millisStr = Long.toString( millis );
605 
606             buf.append( '.' ).append( PADDING_6[millisStr.length() - 1] ).append( millisStr ).append( "Z#" );
607 
608             String countStr = Integer.toHexString( changeCount );
609 
610             buf.append( PADDING_6[countStr.length() - 1] ).append( countStr );
611             buf.append( '#' );
612 
613             String replicaIdStr = Integer.toHexString( replicaId );
614 
615             buf.append( PADDING_3[replicaIdStr.length() - 1] ).append( replicaIdStr );
616             buf.append( '#' );
617 
618             String operationNumberStr = Integer.toHexString( operationNumber );
619 
620             buf.append( PADDING_6[operationNumberStr.length() - 1] ).append( operationNumberStr );
621 
622             csnStr = buf.toString();
623         }
624 
625         return csnStr;
626     }
627 
628 
629     /**
630      * Returns a hash code value for the object.
631      * 
632      * @return a hash code value for this object.
633      */
634     public int hashCode()
635     {
636         int h = 37;
637 
638         h = h * 17 + ( int ) ( timestamp ^ ( timestamp >>> 32 ) );
639         h = h * 17 + changeCount;
640         h = h * 17 + replicaId;
641         h = h * 17 + operationNumber;
642 
643         return h;
644     }
645 
646 
647     /**
648      * Indicates whether some other object is "equal to" this one
649      * 
650      * @param o the reference object with which to compare.
651      * @return <code>true</code> if this object is the same as the obj argument; 
652      * <code>false</code> otherwise.
653      */
654     public boolean equals( Object o )
655     {
656         if ( this == o )
657         {
658             return true;
659         }
660 
661         if ( !( o instanceof Csn ) )
662         {
663             return false;
664         }
665 
666         Csn that = ( Csn ) o;
667 
668         return ( timestamp == that.timestamp ) && ( changeCount == that.changeCount )
669             && ( replicaId == that.replicaId ) && ( operationNumber == that.operationNumber );
670     }
671 
672 
673     /**
674      * Compares this object with the specified object for order.  Returns a
675      * negative integer, zero, or a positive integer as this object is less
676      * than, equal to, or greater than the specified object.<p>
677      * 
678      * @param   csn the Object to be compared.
679      * @return  a negative integer, zero, or a positive integer as this object
680      *      is less than, equal to, or greater than the specified object.
681      */
682     public int compareTo( Csn csn )
683     {
684         if ( csn == null )
685         {
686             return 1;
687         }
688 
689         // Compares the timestamp first
690         if ( this.timestamp < csn.timestamp )
691         {
692             return -1;
693         }
694         else if ( this.timestamp > csn.timestamp )
695         {
696             return 1;
697         }
698 
699         // Then the change count
700         if ( this.changeCount < csn.changeCount )
701         {
702             return -1;
703         }
704         else if ( this.changeCount > csn.changeCount )
705         {
706             return 1;
707         }
708 
709         // Then the replicaId
710         int replicaIdCompareResult = getReplicaIdCompareResult( csn );
711 
712         if ( replicaIdCompareResult != 0 )
713         {
714             return replicaIdCompareResult;
715         }
716 
717         // Last, not least, compares the operation number
718         if ( this.operationNumber < csn.operationNumber )
719         {
720             return -1;
721         }
722         else if ( this.operationNumber > csn.operationNumber )
723         {
724             return 1;
725         }
726         else
727         {
728             return 0;
729         }
730     }
731 
732 
733     private int getReplicaIdCompareResult( Csn csn )
734     {
735         if ( this.replicaId < csn.replicaId )
736         {
737             return -1;
738         }
739         if ( this.replicaId > csn.replicaId )
740         {
741             return 1;
742         }
743         return 0;
744     }
745 }