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 *    https://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 final SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT );
084
085    private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
086
087    /** Padding used to format number with a fixed size */
088    private static final String[] PADDING_6 = new String[]
089        { "00000", "0000", "000", "00", "0", "" };
090
091    /** Padding used to format number with a fixed size */
092    private static final String[] PADDING_3 = new String[]
093        { "00", "0", "" };
094
095
096    /**
097     * Creates a new instance.
098     * <b>This method should be used only for deserializing a CSN</b> 
099     * 
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}