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.shared.ldap.model.csn;
021
022
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.Date;
026
027import org.apache.directory.shared.i18n.I18n;
028import org.apache.directory.shared.util.Chars;
029import org.apache.directory.shared.util.DateUtils;
030import org.apache.directory.shared.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    // Initialize the dateFormat with the UTC TZ
085    static
086    {
087        SDF.setTimeZone( DateUtils.UTC_TIME_ZONE );
088    }
089    
090    /** Padding used to format number with a fixed size */
091    private static final String[] PADDING_6 = new String[] { "00000", "0000", "000", "00", "0", "" };
092
093    /** Padding used to format number with a fixed size */
094    private static final String[] PADDING_3 = new String[] { "00", "0", "" };
095
096
097    /**
098     * Creates a new instance.
099     * <b>This method should be used only for deserializing a CSN</b> 
100     * 
101     * @param timestamp GMT timestamp of modification
102     * @param changeCount The operation increment
103     * @param replicaId Replica ID where modification occurred (<tt>[-_A-Za-z0-9]{1,16}</tt>)
104     * @param operationNumber Operation number in a modification operation
105     */
106    public Csn( long timestamp, int changeCount, int replicaId, int operationNumber )
107    {
108        this.timestamp = timestamp;
109        this.replicaId = replicaId;
110        this.operationNumber = operationNumber;
111        this.changeCount = changeCount;
112    }
113
114
115    /**
116     * Creates a new instance of SimpleCSN from a String.
117     * 
118     * The string format must be :
119     * &lt;timestamp> # &lt;changeCount> # &lt;replica ID> # &lt;operation number>
120     *
121     * @param value The String containing the CSN
122     * @throws InvalidCSNException if the value doesn't contain a valid CSN
123     */
124    public Csn( String value ) throws InvalidCSNException
125    {
126        if ( Strings.isEmpty(value) )
127        {
128            String message = I18n.err( I18n.ERR_04114 );
129            LOG.error( message );
130            throw new InvalidCSNException( message );
131        }
132        
133        if ( value.length() != 40 )
134        {
135            String message = I18n.err( I18n.ERR_04115 );
136            LOG.error( message );
137            throw new InvalidCSNException( message );
138        }
139
140        // Get the Timestamp
141        int sepTS = value.indexOf( '#' );
142        
143        if ( sepTS < 0 )
144        {
145            String message = I18n.err( I18n.ERR_04116 );
146            LOG.error( message );
147            throw new InvalidCSNException( message );
148        }
149        
150        String timestampStr = value.substring( 0, sepTS ).trim();
151        
152        if ( timestampStr.length() != 22 )
153        {
154            String message = I18n.err( I18n.ERR_04117 );
155            LOG.error( message );
156            throw new InvalidCSNException( message );
157        }
158        
159        // Let's transform the Timestamp by removing the mulliseconds and microseconds
160        String realTimestamp = timestampStr.substring( 0, 14 );
161        
162        long tempTimestamp = 0L;
163        
164        synchronized ( SDF )
165        {
166            try
167            {
168                tempTimestamp = SDF.parse( realTimestamp ).getTime();
169            }
170            catch ( ParseException pe )
171            {
172                String message = I18n.err( I18n.ERR_04118, timestampStr );
173                LOG.error( message );
174                throw new InvalidCSNException( message );
175            }
176        }
177        
178        int millis = 0;
179        
180        // And add the milliseconds and microseconds now
181        try
182        {
183            millis = Integer.valueOf( timestampStr.substring( 15, 21 ) );
184        }
185        catch ( NumberFormatException nfe )
186        {
187            String message = I18n.err( I18n.ERR_04119 );
188            LOG.error( message );
189            throw new InvalidCSNException( message );
190        }
191        
192        tempTimestamp += (millis/1000);
193        timestamp = tempTimestamp;
194
195        // Get the changeCount. It should be an hex number prefixed with '0x'
196        int sepCC = value.indexOf( '#', sepTS + 1 );
197        
198        if ( sepCC < 0 )
199        {
200            String message = I18n.err( I18n.ERR_04110, value );
201            LOG.error( message );
202            throw new InvalidCSNException( message );
203        }
204
205        String changeCountStr = value.substring( sepTS + 1, sepCC ).trim();
206        
207        try
208        {
209            changeCount = Integer.parseInt( changeCountStr, 16 ); 
210        }
211        catch ( NumberFormatException nfe )
212        {
213            String message = I18n.err( I18n.ERR_04121, changeCountStr );
214            LOG.error( message );
215            throw new InvalidCSNException( message );
216        }
217        
218        // Get the replicaID
219        int sepRI = value.indexOf( '#', sepCC + 1 );
220        
221        if ( sepRI < 0 )
222        {
223            String message = I18n.err( I18n.ERR_04122, value );
224            LOG.error( message );
225            throw new InvalidCSNException( message );
226        }
227
228        String replicaIdStr = value.substring( sepCC + 1, sepRI).trim();
229        
230        if ( Strings.isEmpty(replicaIdStr) )
231        {
232            String message = I18n.err( I18n.ERR_04123 );
233            LOG.error( message );
234            throw new InvalidCSNException( message );
235        }
236        
237        try
238        {
239            replicaId = Integer.parseInt( replicaIdStr, 16 ); 
240        }
241        catch ( NumberFormatException nfe )
242        {
243            String message = I18n.err( I18n.ERR_04124, replicaIdStr );
244            LOG.error( message );
245            throw new InvalidCSNException( message );
246        }
247        
248        // Get the modification number
249        if ( sepCC == value.length() )
250        {
251            String message = I18n.err( I18n.ERR_04125 );
252            LOG.error( message );
253            throw new InvalidCSNException( message );
254        }
255        
256        String operationNumberStr = value.substring( sepRI + 1 ).trim();
257        
258        try
259        {
260            operationNumber = Integer.parseInt( operationNumberStr, 16 ); 
261        }
262        catch ( NumberFormatException nfe )
263        {
264            String message =  I18n.err( I18n.ERR_04126, operationNumberStr );
265            LOG.error( message );
266            throw new InvalidCSNException( message );
267        }
268        
269        csnStr = value;
270        bytes = Strings.getBytesUtf8(csnStr);
271    }
272
273
274    /**
275     * Check if the given String is a valid CSN.
276     * 
277     * @param value The String to check
278     * @return <code>true</code> if the String is a valid CSN
279     */
280    public static boolean isValid( String value )
281    {
282        if ( Strings.isEmpty(value) )
283        {
284            return false;
285        }
286        
287        if ( value.length() != 40 )
288        {
289            return false;
290        }
291    
292        // Get the Timestamp
293        int sepTS = value.indexOf( '#' );
294        
295        if ( sepTS < 0 )
296        {
297            return false;
298        }
299        
300        String timestampStr = value.substring( 0, sepTS ).trim();
301        
302        if ( timestampStr.length() != 22 )
303        {
304            return false;
305        }
306        
307        // Let's transform the Timestamp by removing the mulliseconds and microseconds
308        String realTimestamp = timestampStr.substring( 0, 14 );
309        
310        synchronized ( SDF )
311        {
312            try
313            {
314                SDF.parse( realTimestamp ).getTime();
315            }
316            catch ( ParseException pe )
317            {
318                return false;
319            }
320        }
321        
322        // And add the milliseconds and microseconds now
323        String millisStr = timestampStr.substring( 15, 21 );
324
325        if ( Strings.isEmpty(millisStr) )
326        {
327            return false;
328        }
329        
330        for ( int i = 0; i < 6; i++ )
331        {
332            if ( !Chars.isDigit(millisStr, i) )
333            {
334                return false;
335            }
336        }
337
338        try
339        {
340            Integer.valueOf( millisStr );
341        }
342        catch ( NumberFormatException nfe )
343        {
344            return false;
345        }
346    
347        // Get the changeCount. It should be an hex number prefixed with '0x'
348        int sepCC = value.indexOf( '#', sepTS + 1 );
349        
350        if ( sepCC < 0 )
351        {
352            return false;
353        }
354    
355        String changeCountStr = value.substring( sepTS + 1, sepCC ).trim();
356        
357        if ( Strings.isEmpty(changeCountStr) )
358        {
359            return false;
360        }
361        
362        if ( changeCountStr.length() != 6 )
363        {
364            return false;
365        }
366        
367        try
368        {
369            for ( int i = 0; i < 6; i++ )
370            {
371                if ( !Chars.isHex(changeCountStr, i) )
372                {
373                    return false;
374                }
375            }
376            
377            Integer.parseInt( changeCountStr, 16 ); 
378        }
379        catch ( NumberFormatException nfe )
380        {
381            return false;
382        }
383        
384        // Get the replicaIDfalse
385        int sepRI = value.indexOf( '#', sepCC + 1 );
386        
387        if ( sepRI < 0 )
388        {
389            return false;
390        }
391    
392        String replicaIdStr = value.substring( sepCC + 1, sepRI ).trim();
393        
394        if ( Strings.isEmpty(replicaIdStr) )
395        {
396            return false;
397        }
398        
399        if ( replicaIdStr.length() != 3 )
400        {
401            return false;
402        }
403        
404        for ( int i = 0; i < 3; i++ )
405        {
406            if ( !Chars.isHex(replicaIdStr, i) )
407            {
408                return false;
409            }
410        }
411
412        try
413        {
414            Integer.parseInt( replicaIdStr, 16 ); 
415        }
416        catch ( NumberFormatException nfe )
417        {
418            return false;
419        }
420        
421        // Get the modification number
422        if ( sepCC == value.length() )
423        {
424            return false;
425        }
426        
427        String operationNumberStr = value.substring( sepRI + 1 ).trim();
428        
429        if ( operationNumberStr.length() != 6 )
430        {
431            return false;
432        }
433
434        for ( int i = 0; i < 6; i++ )
435        {
436            if ( !Chars.isHex(operationNumberStr, i) )
437            {
438                return false;
439            }
440        }
441
442        try
443        {
444            Integer.parseInt( operationNumberStr, 16 ); 
445        }
446        catch ( NumberFormatException nfe )
447        {
448            return false;
449        }
450        
451        return true;
452    }
453
454
455    /**
456     * Creates a new instance of SimpleCSN from the serialized data
457     *
458     * @param value The byte array which contains the serialized CSN
459     */
460    Csn( byte[] value )
461    {
462        csnStr = Strings.utf8ToString(value);
463        Csn csn = new Csn( csnStr );
464        timestamp = csn.timestamp;
465        changeCount = csn.changeCount;
466        replicaId = csn.replicaId;
467        operationNumber = csn.operationNumber;
468        bytes = Strings.getBytesUtf8(csnStr);
469    }
470
471
472    /**
473     * Get the CSN as a byte array. The data are stored as :
474     * bytes 1 to 8  : timestamp, big-endian
475     * bytes 9 to 12 : change count, big endian
476     * bytes 13 to ... : ReplicaId 
477     * 
478     * @return A copy of the byte array representing theCSN
479     */
480    public byte[] getBytes()
481    {
482        if ( bytes == null )
483        {
484            bytes = Strings.getBytesUtf8(csnStr);
485        }
486
487        byte[] copy = new byte[bytes.length];
488        System.arraycopy( bytes, 0, copy, 0, bytes.length );
489        return copy;
490    }
491
492
493    /**
494     * @return The timestamp
495     */
496    public long getTimestamp()
497    {
498        return timestamp;
499    }
500
501
502    /**
503     * @return The changeCount
504     */
505    public int getChangeCount()
506    {
507        return changeCount;
508    }
509
510
511    /**
512     * @return The replicaId
513     */
514    public int getReplicaId()
515    {
516        return replicaId;
517    }
518
519
520    /**
521     * @return The operation number
522     */
523    public int getOperationNumber()
524    {
525        return operationNumber;
526    }
527
528
529    /**
530     * @return The CSN as a String
531     */
532    public String toString()
533    {
534        if ( csnStr == null )
535        {
536            StringBuilder buf = new StringBuilder( 40 );
537            
538            synchronized( SDF )
539            {
540                buf.append( SDF.format( new Date( timestamp ) ) );
541            }
542            
543            // Add the milliseconds part
544            long millis = (timestamp % 1000 ) * 1000;
545            String millisStr = Long.toString( millis );
546            
547            buf.append( '.' ).append( PADDING_6[ millisStr.length() - 1 ] ).append( millisStr ).append( "Z#" );
548            
549            String countStr = Integer.toHexString( changeCount );
550            
551            buf.append( PADDING_6[countStr.length() - 1] ).append( countStr );
552            buf.append( '#' );
553
554            String replicaIdStr = Integer.toHexString( replicaId );
555            
556            buf.append( PADDING_3[replicaIdStr.length() - 1]).append( replicaIdStr );
557            buf.append( '#' );
558            
559            String operationNumberStr = Integer.toHexString( operationNumber );
560            
561            buf.append( PADDING_6[operationNumberStr.length() - 1] ).append( operationNumberStr );
562            
563            csnStr = buf.toString();
564        }
565        
566        return csnStr;
567    }
568
569
570    /**
571     * Returns a hash code value for the object.
572     * 
573     * @return a hash code value for this object.
574     */
575    public int hashCode()
576    {
577        int h = 37;
578        
579        h = h*17 + (int)(timestamp ^ (timestamp >>> 32));
580        h = h*17 + changeCount;
581        h = h*17 + replicaId;
582        h = h*17 + operationNumber;
583        
584        return h;
585    }
586
587
588    /**
589     * Indicates whether some other object is "equal to" this one
590     * 
591     * @param o the reference object with which to compare.
592     * @return <code>true</code> if this object is the same as the obj argument; 
593     * <code>false</code> otherwise.
594     */
595    public boolean equals( Object o )
596    {
597        if ( this == o )
598        {
599            return true;
600        }
601
602        if ( !( o instanceof Csn ) )
603        {
604            return false;
605        }
606
607        Csn that = ( Csn ) o;
608
609        return ( timestamp == that.timestamp ) && ( changeCount == that.changeCount )
610            && ( replicaId == that.replicaId ) && ( operationNumber == that.operationNumber );
611    }
612
613
614    /**
615     * Compares this object with the specified object for order.  Returns a
616     * negative integer, zero, or a positive integer as this object is less
617     * than, equal to, or greater than the specified object.<p>
618     * 
619     * @param   csn the Object to be compared.
620     * @return  a negative integer, zero, or a positive integer as this object
621     *      is less than, equal to, or greater than the specified object.
622     */
623    public int compareTo( Csn csn )
624    {
625        if ( csn == null )
626        {
627            return 1;
628        }
629        
630        // Compares the timestamp first
631        if ( this.timestamp < csn.timestamp )
632        {
633            return -1;
634        }
635        else if ( this.timestamp > csn.timestamp )
636        {
637            return 1;
638        }
639
640        // Then the change count
641        if ( this.changeCount < csn.changeCount )
642        {
643            return -1;
644        }
645        else if ( this.changeCount > csn.changeCount )
646        {
647            return 1;
648        }
649
650        // Then the replicaId
651        int replicaIdCompareResult = getReplicaIdCompareResult( csn );
652
653        if ( replicaIdCompareResult != 0 )
654        {
655            return replicaIdCompareResult;
656        }
657
658        // Last, not least, compares the operation number
659        if ( this.operationNumber < csn.operationNumber )
660        {
661            return -1;
662        }
663        else if ( this.operationNumber > csn.operationNumber )
664        {
665            return 1;
666        }
667        else
668        {
669            return 0;
670        }
671    }
672
673
674    private int getReplicaIdCompareResult( Csn csn )
675    {
676        if ( this.replicaId < csn.replicaId )
677        {
678            return -1;
679        }
680        if ( this.replicaId > csn.replicaId )
681        {
682            return 1;
683        }
684        return 0;
685    }
686}