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         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 }