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