blob: 8ddcd7e7987b54a0c81cbdf9cb77b6107ed24726 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.directory.api.ldap.model.csn;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.util.Chars;
import org.apache.directory.api.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents 'Change Sequence Number' in LDUP specification.
*
* A CSN is a composition of a timestamp, a replica ID and a
* operation sequence number.
*
* It's described in http://tools.ietf.org/html/draft-ietf-ldup-model-09.
*
* The CSN syntax is :
* <pre>
* &lt;CSN&gt; ::= &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replicaId&gt; # &lt;modifierNumber&gt;
* &lt;timestamp&gt; ::= A GMT based time, YYYYmmddHHMMSS.uuuuuuZ
* &lt;changeCount&gt; ::= [000000-ffffff]
* &lt;replicaId&gt; ::= [000-fff]
* &lt;modifierNumber&gt; ::= [000000-ffffff]
* </pre>
*
* It distinguishes a change made on an object on a server,
* and if two operations take place during the same timeStamp,
* the operation sequence number makes those operations distinct.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class Csn implements Comparable<Csn>
{
/** The logger for this class */
private static final Logger LOG = LoggerFactory.getLogger( Csn.class );
/** The timeStamp of this operation */
private final long timestamp;
/** The server identification */
private final int replicaId;
/** The operation number in a modification operation */
private final int operationNumber;
/** The changeCount to distinguish operations done in the same second */
private final int changeCount;
/** Stores the String representation of the CSN */
private String csnStr;
/** Stores the byte array representation of the CSN */
private byte[] bytes;
/** The Timestamp syntax. The last 'z' is _not_ the Time Zone */
private final SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMddHHmmss", Locale.ROOT );
private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone( "UTC" );
/** Padding used to format number with a fixed size */
private static final String[] PADDING_6 = new String[]
{ "00000", "0000", "000", "00", "0", "" };
/** Padding used to format number with a fixed size */
private static final String[] PADDING_3 = new String[]
{ "00", "0", "" };
/**
* Creates a new instance.
* <b>This method should be used only for deserializing a CSN</b>
*
* @param timestamp GMT timestamp of modification
* @param changeCount The operation increment
* @param replicaId Replica ID where modification occurred (<tt>[-_A-Za-z0-9]{1,16}</tt>)
* @param operationNumber Operation number in a modification operation
*/
public Csn( long timestamp, int changeCount, int replicaId, int operationNumber )
{
this.timestamp = timestamp;
this.replicaId = replicaId;
this.operationNumber = operationNumber;
this.changeCount = changeCount;
sdf.setTimeZone( UTC_TIME_ZONE );
}
/**
* Creates a new instance of SimpleCSN from a String.
*
* The string format must be :
* &lt;timestamp&gt; # &lt;changeCount&gt; # &lt;replica ID&gt; # &lt;operation number&gt;
*
* @param value The String containing the CSN
*/
public Csn( String value )
{
sdf.setTimeZone( UTC_TIME_ZONE );
if ( Strings.isEmpty( value ) )
{
String message = I18n.err( I18n.ERR_13015_NULL_OR_EMPTY_CSN );
LOG.error( message );
throw new InvalidCSNException( message );
}
if ( value.length() != 40 )
{
String message = I18n.err( I18n.ERR_13016_INCORRECT_CSN_LENGTH );
LOG.error( message );
throw new InvalidCSNException( message );
}
// Get the Timestamp
int sepTS = value.indexOf( '#' );
if ( sepTS < 0 )
{
String message = I18n.err( I18n.ERR_13017_CANT_FIND_SHARP_IN_CSN );
LOG.error( message );
throw new InvalidCSNException( message );
}
String timestampStr = value.substring( 0, sepTS ).trim();
if ( timestampStr.length() != 22 )
{
String message = I18n.err( I18n.ERR_13018_TIMESTAMP_NOT_LONG_ENOUGH );
LOG.error( message );
throw new InvalidCSNException( message );
}
// Let's transform the Timestamp by removing the mulliseconds and microseconds
String realTimestamp = timestampStr.substring( 0, 14 );
long tempTimestamp = 0L;
synchronized ( sdf )
{
try
{
tempTimestamp = sdf.parse( realTimestamp ).getTime();
}
catch ( ParseException pe )
{
String message = I18n.err( I18n.ERR_13019_CANNOT_PARSE_TIMESTAMP, timestampStr );
LOG.error( message );
throw new InvalidCSNException( message, pe );
}
}
int millis = 0;
// And add the milliseconds and microseconds now
try
{
millis = Integer.parseInt( timestampStr.substring( 15, 21 ) );
}
catch ( NumberFormatException nfe )
{
String message = I18n.err( I18n.ERR_13020_INVALID_MICROSECOND );
LOG.error( message );
throw new InvalidCSNException( message, nfe );
}
tempTimestamp += ( millis / 1000 );
timestamp = tempTimestamp;
// Get the changeCount. It should be an hex number prefixed with '0x'
int sepCC = value.indexOf( '#', sepTS + 1 );
if ( sepCC < 0 )
{
String message = I18n.err( I18n.ERR_13014_DN_ATTR_FLAG_INVALID, value );
LOG.error( message );
throw new InvalidCSNException( message );
}
String changeCountStr = value.substring( sepTS + 1, sepCC ).trim();
try
{
changeCount = Integer.parseInt( changeCountStr, 16 );
}
catch ( NumberFormatException nfe )
{
String message = I18n.err( I18n.ERR_13021_INVALID_CHANGE_COUNT, changeCountStr );
LOG.error( message );
throw new InvalidCSNException( message, nfe );
}
// Get the replicaID
int sepRI = value.indexOf( '#', sepCC + 1 );
if ( sepRI < 0 )
{
String message = I18n.err( I18n.ERR_13022_MISSING_SHARP_IN_CSN, value );
LOG.error( message );
throw new InvalidCSNException( message );
}
String replicaIdStr = value.substring( sepCC + 1, sepRI ).trim();
if ( Strings.isEmpty( replicaIdStr ) )
{
String message = I18n.err( I18n.ERR_13023_REPLICA_ID_NULL );
LOG.error( message );
throw new InvalidCSNException( message );
}
try
{
replicaId = Integer.parseInt( replicaIdStr, 16 );
}
catch ( NumberFormatException nfe )
{
String message = I18n.err( I18n.ERR_13024_INVALID_REPLICA_ID, replicaIdStr );
LOG.error( message );
throw new InvalidCSNException( message, nfe );
}
// Get the modification number
if ( sepCC == value.length() )
{
String message = I18n.err( I18n.ERR_13025_NO_OPERATION_NUMBER );
LOG.error( message );
throw new InvalidCSNException( message );
}
String operationNumberStr = value.substring( sepRI + 1 ).trim();
try
{
operationNumber = Integer.parseInt( operationNumberStr, 16 );
}
catch ( NumberFormatException nfe )
{
String message = I18n.err( I18n.ERR_13026_INVALID_OPERATION_NUMBER, operationNumberStr );
LOG.error( message );
throw new InvalidCSNException( message, nfe );
}
csnStr = value;
bytes = Strings.getBytesUtf8( csnStr );
}
/**
* Creates a new instance of SimpleCSN from the serialized data
*
* @param value The byte array which contains the serialized CSN
*/
Csn( byte[] value )
{
csnStr = Strings.utf8ToString( value );
Csn csn = new Csn( csnStr );
timestamp = csn.timestamp;
changeCount = csn.changeCount;
replicaId = csn.replicaId;
operationNumber = csn.operationNumber;
bytes = Strings.getBytesUtf8( csnStr );
}
/**
* Check if the given String is a valid CSN.
*
* @param value The String to check
* @return <code>true</code> if the String is a valid CSN
*/
public static boolean isValid( String value )
{
if ( Strings.isEmpty( value ) )
{
return false;
}
char[] chars = value.toCharArray();
if ( chars.length != 40 )
{
return false;
}
// Get the Timestamp
// Check the timestamp's year
for ( int pos = 0; pos < 4; pos++ )
{
if ( !Chars.isDigit( chars[pos] ) )
{
return false;
}
}
// Check the timestamp month
switch ( chars[4] )
{
case '0' :
if ( !Chars.isDigit( chars[5] ) )
{
return false;
}
if ( chars[5] == '0' )
{
return false;
}
break;
case '1' :
if ( ( chars[5] != '0' ) && ( chars[5] != '1' ) && ( chars[5] != '2' ) )
{
return false;
}
break;
default :
return false;
}
// Check the timestamp day
switch ( chars[6] )
{
case '0' :
if ( !Chars.isDigit( chars[7] ) )
{
return false;
}
if ( chars[7] == '0' )
{
return false;
}
break;
case '1' :
case '2' : // Special case for february...
if ( !Chars.isDigit( chars[7] ) )
{
return false;
}
break;
case '3' :
// Deal with 30 days months
if ( ( chars[7] != '0' ) && ( chars[7] != '1' ) )
{
return false;
}
break;
default :
return false;
}
// Check the timestamp hour
switch ( chars[8] )
{
case '0' :
case '1' :
if ( !Chars.isDigit( chars[9] ) )
{
return false;
}
break;
case '2' :
if ( ( chars[9] != '0' ) && ( chars[9] != '1' ) && ( chars[9] != '2' ) && ( chars[9] != '3' ) )
{
return false;
}
break;
default :
return false;
}
// Check the timestamp minute
switch ( chars[10] )
{
case '0' :
case '1' :
case '2' :
case '3' :
case '4' :
case '5' :
break;
default :
return false;
}
if ( !Chars.isDigit( chars[11] ) )
{
return false;
}
// Check the timestamp seconds
switch ( chars[12] )
{
case '0' :
case '1' :
case '2' :
case '3' :
case '4' :
case '5' :
break;
default :
return false;
}
if ( !Chars.isDigit( chars[13] ) )
{
return false;
}
// Check the milliseconds
if ( chars[14] != '.' )
{
return false;
}
for ( int i = 0; i < 6; i++ )
{
if ( !Chars.isDigit( chars[15 + i] ) )
{
return false;
}
}
if ( chars[21] != 'Z' )
{
return false;
}
if ( chars[22] != '#' )
{
return false;
}
// Get the changeCount. It should be an 6 digit hex number
if ( !Chars.isHex( ( byte ) chars[23] )
|| !Chars.isHex( ( byte ) chars[24] )
|| !Chars.isHex( ( byte ) chars[25] )
|| !Chars.isHex( ( byte ) chars[26] )
|| !Chars.isHex( ( byte ) chars[27] )
|| !Chars.isHex( ( byte ) chars[28] ) )
{
return false;
}
if ( chars[29] != '#' )
{
return false;
}
// Get the replicaID, which should be a 3 digits hex number
if ( !Chars.isHex( ( byte ) chars[30] )
|| !Chars.isHex( ( byte ) chars[31] )
|| !Chars.isHex( ( byte ) chars[32] ) )
{
return false;
}
if ( chars[33] != '#' )
{
return false;
}
// Check the modification number, which should be a 6 digits hex number
if ( !Chars.isHex( ( byte ) chars[34] )
|| !Chars.isHex( ( byte ) chars[35] )
|| !Chars.isHex( ( byte ) chars[36] )
|| !Chars.isHex( ( byte ) chars[37] )
|| !Chars.isHex( ( byte ) chars[38] )
|| !Chars.isHex( ( byte ) chars[39] ) )
{
return false;
}
return true;
}
/**
* Get the CSN as a byte array. The data are stored as :
* bytes 1 to 8 : timestamp, big-endian
* bytes 9 to 12 : change count, big endian
* bytes 13 to ... : ReplicaId
*
* @return A copy of the byte array representing theCSN
*/
public byte[] getBytes()
{
if ( bytes == null )
{
bytes = Strings.getBytesUtf8( csnStr );
}
byte[] copy = new byte[bytes.length];
System.arraycopy( bytes, 0, copy, 0, bytes.length );
return copy;
}
/**
* @return The timestamp
*/
public long getTimestamp()
{
return timestamp;
}
/**
* @return The changeCount
*/
public int getChangeCount()
{
return changeCount;
}
/**
* @return The replicaId
*/
public int getReplicaId()
{
return replicaId;
}
/**
* @return The operation number
*/
public int getOperationNumber()
{
return operationNumber;
}
/**
* @return The CSN as a String
*/
@Override
public String toString()
{
if ( csnStr == null )
{
StringBuilder buf = new StringBuilder( 40 );
synchronized ( sdf )
{
buf.append( sdf.format( new Date( timestamp ) ) );
}
// Add the milliseconds part
long millis = ( timestamp % 1000 ) * 1000;
String millisStr = Long.toString( millis );
buf.append( '.' ).append( PADDING_6[millisStr.length() - 1] ).append( millisStr ).append( "Z#" );
String countStr = Integer.toHexString( changeCount );
buf.append( PADDING_6[countStr.length() - 1] ).append( countStr );
buf.append( '#' );
String replicaIdStr = Integer.toHexString( replicaId );
buf.append( PADDING_3[replicaIdStr.length() - 1] ).append( replicaIdStr );
buf.append( '#' );
String operationNumberStr = Integer.toHexString( operationNumber );
buf.append( PADDING_6[operationNumberStr.length() - 1] ).append( operationNumberStr );
csnStr = buf.toString();
}
return csnStr;
}
/**
* Returns a hash code value for the object.
*
* @return a hash code value for this object.
*/
@Override
public int hashCode()
{
int h = 37;
h = h * 17 + ( int ) ( timestamp ^ ( timestamp >>> 32 ) );
h = h * 17 + changeCount;
h = h * 17 + replicaId;
h = h * 17 + operationNumber;
return h;
}
/**
* Indicates whether some other object is "equal to" this one
*
* @param o the reference object with which to compare.
* @return <code>true</code> if this object is the same as the obj argument;
* <code>false</code> otherwise.
*/
@Override
public boolean equals( Object o )
{
if ( this == o )
{
return true;
}
if ( !( o instanceof Csn ) )
{
return false;
}
Csn that = ( Csn ) o;
return ( timestamp == that.timestamp ) && ( changeCount == that.changeCount )
&& ( replicaId == that.replicaId ) && ( operationNumber == that.operationNumber );
}
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.<p>
*
* @param csn the Object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*/
@Override
public int compareTo( Csn csn )
{
if ( csn == null )
{
return 1;
}
// Compares the timestamp first
if ( this.timestamp < csn.timestamp )
{
return -1;
}
else if ( this.timestamp > csn.timestamp )
{
return 1;
}
// Then the change count
if ( this.changeCount < csn.changeCount )
{
return -1;
}
else if ( this.changeCount > csn.changeCount )
{
return 1;
}
// Then the replicaId
int replicaIdCompareResult = getReplicaIdCompareResult( csn );
if ( replicaIdCompareResult != 0 )
{
return replicaIdCompareResult;
}
// Last, not least, compares the operation number
if ( this.operationNumber < csn.operationNumber )
{
return -1;
}
else if ( this.operationNumber > csn.operationNumber )
{
return 1;
}
else
{
return 0;
}
}
private int getReplicaIdCompareResult( Csn csn )
{
if ( this.replicaId < csn.replicaId )
{
return -1;
}
if ( this.replicaId > csn.replicaId )
{
return 1;
}
return 0;
}
}