/*
* Copyright 2015 GoDataDriven B.V.
*
* Licensed 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
*
* http://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 io.divolte.server;
import com.google.common.base.Splitter;
import javax.annotation.Nonnull;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Unique time-based identifiers for Divolte.
*
* Divolte uses unique identifiers for several purposes, some of which require
* an embedded timestamp indicating when the identifier was generated. (Although
* we could use Version 1 UUIDs, not all clients can trivially generate these.)
*/
public final class DivolteIdentifier {
private final static char VERSION = '0';
private final static String VERSION_STRING = "" + VERSION;
private static final char SEPARATOR_CHAR = ':';
private final static Splitter splitter = Splitter.on(SEPARATOR_CHAR).limit(4);
/** The unique identifier */
@Nonnull
public final String value;
/**
* The difference, measured in milliseconds by the system that generated the
* identifier, between when the identifier was generated and midnight, January 1, 1970 UTC.
*/
public final long timestamp;
/**
* The version of the identifier.
*/
public final char version;
private DivolteIdentifier(final long timestamp, final String id) {
this.version = VERSION;
this.timestamp = timestamp;
this.value = VERSION_STRING + SEPARATOR_CHAR
+ Long.toString(timestamp, 36) + SEPARATOR_CHAR
+ Objects.requireNonNull(id);
}
@Override
public String toString() {
return value;
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public boolean equals(final Object other) {
return this == other ||
null != other && getClass() == other.getClass() && value.equals(((DivolteIdentifier) other).value);
}
public static Optional<DivolteIdentifier> tryParse(final String input) {
Objects.requireNonNull(input);
try {
final List<String> parts = splitter.splitToList(input);
return parts.size() == 3 && VERSION_STRING.equals(parts.get(0))
? Optional.of(new DivolteIdentifier(Long.parseLong(parts.get(1), 36), parts.get(2)))
: Optional.empty();
} catch (final NumberFormatException e) {
return Optional.empty();
}
}
// Some sources mention it's a good idea to avoid contention on SecureRandom instances...
private final static ThreadLocal<SecureRandom> localRandom = new ThreadLocal<SecureRandom> () {
protected SecureRandom initialValue() {
return new SecureRandom();
}
};
public static DivolteIdentifier generate(final long ts) {
final SecureRandom random = localRandom.get();
final byte[] randomBytes = new byte[24];
random.nextBytes(randomBytes);
final String id = Base64.getUrlEncoder().encodeToString(randomBytes);
return new DivolteIdentifier(ts, id);
}
public static DivolteIdentifier generate() {
return generate(System.currentTimeMillis());
}
}