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