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