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 */
020package org.apache.directory.api.ldap.model.csn;
021
022
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.Date;
026import java.util.Locale;
027import java.util.TimeZone;
028
029import org.apache.directory.api.i18n.I18n;
030import org.apache.directory.api.util.Chars;
031import org.apache.directory.api.util.Strings;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035
036/**
037 * Represents 'Change Sequence Number' in LDUP specification.
038 * 
039 * A CSN is a composition of a timestamp, a replica ID and a 
040 * operation sequence number.
041 * 
042 * It's described in http://tools.ietf.org/html/draft-ietf-ldup-model-09.
043 * 
044 * The CSN syntax is :
045 * <pre>
046 * &lt;CSN&gt;            ::= &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replicaId&gt; # &lt;modifierNumber&gt;
047 * &lt;timestamp&gt;      ::= A GMT based time, YYYYmmddHHMMSS.uuuuuuZ
048 * &lt;changeCount&gt;    ::= [000000-ffffff] 
049 * &lt;replicaId&gt;      ::= [000-fff]
050 * &lt;modifierNumber&gt; ::= [000000-ffffff]
051 * </pre>
052 *  
053 * It distinguishes a change made on an object on a server,
054 * and if two operations take place during the same timeStamp,
055 * the operation sequence number makes those operations distinct.
056 * 
057 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
058 */
059public class Csn implements Comparable<Csn>
060{
061    /** The logger for this class */
062    private static final Logger LOG = LoggerFactory.getLogger( Csn.class );
063
064    /** The timeStamp of this operation */
065    private final long timestamp;
066
067    /** The server identification */
068    private final int replicaId;
069
070    /** The operation number in a modification operation */
071    private final int operationNumber;
072
073    /** The changeCount to distinguish operations done in the same second */
074    private final int changeCount;
075
076    /** Stores the String representation of the CSN */
077    private String csnStr;
078
079    /** Stores the byte array representation of the CSN */
080    private byte[] bytes;
081
082    /** The Timestamp syntax. The last 'z' is _not_ the Time Zone */
083    private static final SimpleDateFormat SDF = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT );
084
085    private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
086
087    // Initialize the dateFormat with the UTC TZ
088    static
089    {
090        SDF.setTimeZone( UTC_TIME_ZONE );
091    }
092
093    /** Padding used to format number with a fixed size */
094    private static final String[] PADDING_6 = new String[]
095        { "00000", "0000", "000", "00", "0", "" };
096
097    /** Padding used to format number with a fixed size */
098    private static final String[] PADDING_3 = new String[]
099        { "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}