SyncToken.java
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.data.appconfiguration.implementation;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
/**
* Due to the nature of some distributed systems real-time consistency between requests can't (or it's very hard) to be
* enforced implicitly. A solution is to allow protocol support in the form of multiple Synchronization Tokens.
* Synchronization tokens are optional.
* <p>
* Uses optional Sync-Token request/response headers will guarantee real-time consistency between different client
* instances and requests.
*
* @see <a href="https://github.com/Azure/AppConfiguration/blob/master/docs/REST/consistency.md">Real-time Consistency</a>
*/
public final class SyncToken {
private static final String CANNOT_EMPTY_OR_NULL = "sync-token cannot be null or empty.";
private static final String EQUAL = "=";
private static final String SEMICOLON = ";";
private static final String SEQUENCE_NUMBER_CANNOT_PARSED = "Sequence number cannot be parsed to long.";
private static final String VALID_FORMAT_ERROR_MESSAGE =
"Expected sync-token valid format should be <id>=<value>;sn=<sn>. For multiple sync tokens, "
+ "<id>=<value>;sn=<sn>,<id>=<value>;sn=<sn>.";
private static final ClientLogger LOGGER = new ClientLogger(SyncToken.class);
private String id;
private String value;
private long sequenceNumber;
/**
* Create an instance of SyncToken class. Skip the sync token if the parsing failed.
*
* @param syncToken only one raw sync-token string from HTTP response header, ex.,
* <p>Sync-Token: {@code <id>=<value>;sn=<sn></p>}. But not
* <p>Sync-Token: {@code <id>=<value>;sn=<sn>,<id>=<value>;sn=<sn>}</p>
* Allows for better concurrency and client cache ability. The client may choose to use only token's last version,
* since token versions are inclusive. Not required for requests.
*/
public static SyncToken createSyncToken(String syncToken) {
final SyncToken token = new SyncToken();
if (CoreUtils.isNullOrEmpty(syncToken)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(CANNOT_EMPTY_OR_NULL));
}
final String[] syncTokenParts = syncToken.split(SEMICOLON, 2);
if (syncTokenParts.length != 2) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(VALID_FORMAT_ERROR_MESSAGE));
}
final String[] idParts = syncTokenParts[0].split(EQUAL, 2);
final String[] snParts = syncTokenParts[1].split(EQUAL, 2);
if (idParts.length != 2 || snParts.length != 2
|| idParts[0].isEmpty() || idParts[1].isEmpty()
|| snParts[0].isEmpty() || snParts[1].isEmpty()) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(VALID_FORMAT_ERROR_MESSAGE));
}
try {
token.sequenceNumber = Long.parseLong(snParts[1]);
} catch (NumberFormatException ex) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(SEQUENCE_NUMBER_CANNOT_PARSED));
}
token.id = idParts[0];
token.value = idParts[1];
return token;
}
/**
* Get Token ID (opaque).
*
* @return Token ID
*/
public String getId() {
return id;
}
/**
* Get Token value (opaque). Allows base64 encoded string.
*
* @return Token value
*/
public String getValue() {
return value;
}
/**
* Get Token sequence number (version).
*
* Higher means newer version of the same token. Allows for better concurrency
* and client cache ability. The client may choose to use only token's last version, since token versions are
* inclusive. Not required for requests.
*
* @return Token sequence number
*/
public long getSequenceNumber() {
return sequenceNumber;
}
}