mirror of
https://github.com/google/nomulus.git
synced 2025-04-29 19:47:51 +02:00
Add VKey to String and String to VKey methods (#1396)
* Add stringify and parse methods to SerializeUTils * Improve comments and test cases * Fix comments and test strings * Fix dependency warning
This commit is contained in:
parent
230daeeab7
commit
adb82565db
6 changed files with 325 additions and 1 deletions
|
@ -18,10 +18,13 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.BackupGroupRoot;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.translators.VKeyTranslatorFactory;
|
||||
import google.registry.util.SerializeUtils;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -36,6 +39,15 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
|||
|
||||
private static final long serialVersionUID = -5291472863840231240L;
|
||||
|
||||
// Info that's stored in in vkey string generated via stringify().
|
||||
private static final String SQL_LOOKUP_KEY = "sql";
|
||||
private static final String OFY_LOOKUP_KEY = "ofy";
|
||||
private static final String CLASS_TYPE = "kind";
|
||||
|
||||
// Web safe delimiters that won't be used in base 64.
|
||||
private static final String KV_SEPARATOR = ":";
|
||||
private static final String DELIMITER = "@";
|
||||
|
||||
// The SQL key for the referenced entity.
|
||||
Serializable sqlKey;
|
||||
|
||||
|
@ -114,6 +126,47 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
|||
return new VKey<T>(kind, Key.create(kind, name), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link VKey} from the string representation of a vkey.
|
||||
*
|
||||
* <p>There are two types of string representations: 1) existing ofy key string handled by
|
||||
* fromWebsafeKey() and 2) string encoded via stringify() where @ separates the substrings and
|
||||
* each of the substrings contains a look up key, ":", and its corresponding value. The key info
|
||||
* is encoded via Base64. The string begins with "kind:" and it must contains at least ofy key or
|
||||
* sql key.
|
||||
*
|
||||
* <p>Example of a Vkey string by fromWebsafeKey(): "agR0ZXN0chYLEgpEb21haW5CYXNlIgZST0lELTEM"
|
||||
*
|
||||
* <p>Example of a vkey string by stringify(): "google.registry.testing.TestObject@sql:rO0ABX" +
|
||||
* "QAA2Zvbw@ofy:agR0ZXN0cjELEg9FbnRpdHlHcm91cFJvb3QiCWNyb3NzLXRsZAwLEgpUZXN0T2JqZWN0IgNmb28M",
|
||||
* where sql key and ofy key are values are encoded in Base64.
|
||||
*/
|
||||
public static <T> VKey<T> create(String keyString) throws Exception {
|
||||
if (!keyString.startsWith(CLASS_TYPE + KV_SEPARATOR)) {
|
||||
// to handle the existing ofy key string
|
||||
return fromWebsafeKey(keyString);
|
||||
} else {
|
||||
ImmutableMap<String, String> kvs =
|
||||
ImmutableMap.copyOf(
|
||||
Splitter.on(DELIMITER).withKeyValueSeparator(KV_SEPARATOR).split(keyString));
|
||||
Class classType = Class.forName(kvs.get(CLASS_TYPE));
|
||||
|
||||
if (kvs.containsKey(SQL_LOOKUP_KEY) && kvs.containsKey(OFY_LOOKUP_KEY)) {
|
||||
return VKey.create(
|
||||
classType,
|
||||
SerializeUtils.parse(Serializable.class, kvs.get(SQL_LOOKUP_KEY)),
|
||||
Key.create(kvs.get(OFY_LOOKUP_KEY)));
|
||||
} else if (kvs.containsKey(SQL_LOOKUP_KEY)) {
|
||||
return VKey.createSql(
|
||||
classType, SerializeUtils.parse(Serializable.class, kvs.get(SQL_LOOKUP_KEY)));
|
||||
} else if (kvs.containsKey(OFY_LOOKUP_KEY)) {
|
||||
return VKey.createOfy(classType, Key.create(kvs.get(OFY_LOOKUP_KEY)));
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("Cannot parse key string: %s", keyString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone with an ofy key restored from {@code ancestors}.
|
||||
*
|
||||
|
@ -233,4 +286,29 @@ public class VKey<T> extends ImmutableObject implements Serializable {
|
|||
public static <T> VKey<T> fromWebsafeKey(String ofyKeyRepr) {
|
||||
return from(Key.create(ofyKeyRepr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the string representation of a {@link VKey}.
|
||||
*
|
||||
* <p>The string representation of a vkey contains its type, and sql key or ofy key, or both. Each
|
||||
* of the keys is first serialized into a byte array then encoded via Base64 into a web safe
|
||||
* string.
|
||||
*
|
||||
* <p>The string representation of a vkey contains key values pairs separated by delimiter "@".
|
||||
* Another delimiter ":" is put in between each key and value. The following is the complete
|
||||
* format of the string: "kind:class_name@sql:encoded_sqlKey@ofy:encoded_ofyKey", where kind is
|
||||
* required. The string representation may contain an encoded ofy key, or an encoded sql key, or
|
||||
* both.
|
||||
*/
|
||||
public String stringify() {
|
||||
// class type is required to create a vkey
|
||||
String key = CLASS_TYPE + KV_SEPARATOR + getKind().getName();
|
||||
if (maybeGetSqlKey().isPresent()) {
|
||||
key += DELIMITER + SQL_LOOKUP_KEY + KV_SEPARATOR + SerializeUtils.stringify(getSqlKey());
|
||||
}
|
||||
if (maybeGetOfyKey().isPresent()) {
|
||||
key += DELIMITER + OFY_LOOKUP_KEY + KV_SEPARATOR + getOfyKey().getString();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,10 @@ import com.googlecode.objectify.annotation.Entity;
|
|||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.model.translators.VKeyTranslatorFactory;
|
||||
import google.registry.testing.AppEngineExtension;
|
||||
import google.registry.testing.TestObject;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
|
@ -39,6 +41,11 @@ class VKeyTest {
|
|||
.withOfyTestEntities(TestObject.class)
|
||||
.build();
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
VKeyTranslatorFactory.addTestEntityClass(TestObject.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOptionalAccessors() {
|
||||
VKey<TestObject> key =
|
||||
|
@ -130,6 +137,176 @@ class VKeyTest {
|
|||
assertThat(vkey.getSqlKey()).isEqualTo("ROID-1");
|
||||
}
|
||||
|
||||
/** Test stringify() with vkey created via different ways. */
|
||||
@Test
|
||||
void testStringify_sqlOnlyVKey() throws Exception {
|
||||
assertThat(VKey.createSql(TestObject.class, "foo").stringify())
|
||||
.isEqualTo("kind:google.registry.testing.TestObject@sql:rO0ABXQAA2Zvbw");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringify_ofyOnlyVKey() throws Exception {
|
||||
assertThat(VKey.createOfy(TestObject.class, Key.create(TestObject.class, "foo")).stringify())
|
||||
.isEqualTo(
|
||||
"kind:google.registry.testing.TestObject@ofy:agR0ZXN0chMLEgpUZXN0T2JqZWN0IgNmb28M");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringify_vkeyFromWebsafeKey() throws Exception {
|
||||
DomainBase domain = newDomainBase("example.com", "ROID-1", persistActiveContact("contact-1"));
|
||||
Key<DomainBase> key = Key.create(domain);
|
||||
VKey<DomainBase> vkey = VKey.fromWebsafeKey(key.getString());
|
||||
assertThat(vkey.stringify())
|
||||
.isEqualTo(
|
||||
"kind:google.registry.model.domain.DomainBas"
|
||||
+ "e@sql:rO0ABXQABlJPSUQtMQ"
|
||||
+ "@ofy:agR0ZXN0chYLEgpEb21haW5CYXNlIgZST0lELTEM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringify_sqlAndOfyVKey() throws Exception {
|
||||
assertThat(
|
||||
VKey.create(TestObject.class, "foo", Key.create(TestObject.create("foo"))).stringify())
|
||||
.isEqualTo(
|
||||
"kind:google.registry.testing.TestObject@sql:rO0ABXQAA2Zvbw@ofy:agR0ZXN0cjELEg9FbnRpdH"
|
||||
+ "lHcm91cFJvb3QiCWNyb3NzLXRsZAwLEgpUZXN0T2JqZWN0IgNmb28M");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringify_asymmetricVKey() throws Exception {
|
||||
assertThat(
|
||||
VKey.create(TestObject.class, "test", Key.create(TestObject.create("foo"))).stringify())
|
||||
.isEqualTo(
|
||||
"kind:google.registry.testing.TestObject@sql:rO0ABXQABHRlc3Q@ofy:agR0ZXN0cjELEg9FbnRpd"
|
||||
+ "HlHcm91cFJvb3QiCWNyb3NzLXRsZAwLEgpUZXN0T2JqZWN0IgNmb28M");
|
||||
}
|
||||
|
||||
/** Test create() via different vkey string representations. */
|
||||
@Test
|
||||
void testCreate_stringifedVKey_sqlOnlyVKeyString() throws Exception {
|
||||
assertThat(VKey.create("kind:google.registry.testing.TestObject@sql:rO0ABXQAA2Zvbw"))
|
||||
.isEqualTo(VKey.createSql(TestObject.class, "foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_stringifedVKey_ofyOnlyVKeyString() throws Exception {
|
||||
assertThat(
|
||||
VKey.create(
|
||||
"kind:google.registry.testing.TestObject@ofy:agR0ZXN0chMLEgpUZXN0T2JqZWN0IgNmb28M"))
|
||||
.isEqualTo(VKey.createOfy(TestObject.class, Key.create(TestObject.class, "foo")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_stringifedVKey_asymmetricVKeyString() throws Exception {
|
||||
assertThat(
|
||||
VKey.create(
|
||||
"kind:google.registry.testing.TestObject@sql:rO0ABXQABHRlc3Q@ofy:agR0ZXN0cjELEg9Fb"
|
||||
+ "nRpdHlHcm91cFJvb3QiCWNyb3NzLXRsZAwLEgpUZXN0T2JqZWN0IgNmb28M"))
|
||||
.isEqualTo(VKey.create(TestObject.class, "test", Key.create(TestObject.create("foo"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_stringifedVKey_sqlAndOfyVKeyString() throws Exception {
|
||||
assertThat(
|
||||
VKey.create(
|
||||
"kind:google.registry.testing.TestObject@sql:rO0ABXQAA2Zvbw@ofy:agR0ZXN0cjELEg9Fbn"
|
||||
+ "RpdHlHcm91cFJvb3QiCWNyb3NzLXRsZAwLEgpUZXN0T2JqZWN0IgNmb28M"))
|
||||
.isEqualTo(VKey.create(TestObject.class, "foo", Key.create(TestObject.create("foo"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_stringifyVkey_fromWebsafeKey() throws Exception {
|
||||
assertThat(
|
||||
VKey.create(
|
||||
"kind:google.registry.model.domain.DomainBase@sql:rO0ABXQABlJPSUQtMQ"
|
||||
+ "@ofy:agR0ZXN0chYLEgpEb21haW5CYXNlIgZST0lELTEM"))
|
||||
.isEqualTo(
|
||||
VKey.fromWebsafeKey(
|
||||
Key.create(
|
||||
newDomainBase("example.com", "ROID-1", persistActiveContact("contact-1")))
|
||||
.getString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_stringifedVKey_websafeKey() throws Exception {
|
||||
assertThat(VKey.create("agR0ZXN0chYLEgpEb21haW5CYXNlIgZST0lELTEM"))
|
||||
.isEqualTo(VKey.fromWebsafeKey("agR0ZXN0chYLEgpEb21haW5CYXNlIgZST0lELTEM"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_invalidStringifiedVKey_failure() throws Exception {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> VKey.create("kind:google.registry.testing.TestObject@sq:l@ofya:bc"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Cannot parse key string: kind:google.registry.testing.TestObject@sq:l@ofya:bc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_invalidOfyKeyString_failure() throws Exception {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> VKey.create("invalid"));
|
||||
assertThat(thrown).hasMessageThat().contains("Could not parse Reference");
|
||||
}
|
||||
|
||||
/** Test stringify() then create() flow. */
|
||||
@Test
|
||||
void testStringifyThenCreate_sqlOnlyVKey_testObject_stringKey_success() throws Exception {
|
||||
VKey<TestObject> vkey = VKey.createSql(TestObject.class, "foo");
|
||||
VKey<TestObject> newVkey = VKey.create(vkey.stringify());
|
||||
assertThat(newVkey).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyThenCreate_sqlOnlyVKey_testObject_longKey_success() throws Exception {
|
||||
VKey<TestObject> vkey = VKey.createSql(TestObject.class, (long) 12345);
|
||||
VKey<TestObject> newVkey = VKey.create(vkey.stringify());
|
||||
assertThat(newVkey).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreate_createFromExistingOfyKey_success() throws Exception {
|
||||
String keyString =
|
||||
Key.create(newDomainBase("example.com", "ROID-1", persistActiveContact("contact-1")))
|
||||
.getString();
|
||||
assertThat(VKey.fromWebsafeKey(keyString)).isEqualTo(VKey.create(keyString));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyThenCreate_ofyOnlyVKey_testObject_success() throws Exception {
|
||||
VKey<TestObject> vkey =
|
||||
VKey.createOfy(TestObject.class, Key.create(TestObject.class, "tmpKey"));
|
||||
assertThat(VKey.create(vkey.stringify())).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyThenCreate_ofyOnlyVKey_testObject_websafeString_success() throws Exception {
|
||||
VKey<TestObject> vkey = VKey.fromWebsafeKey(Key.create(TestObject.create("foo")).getString());
|
||||
assertThat(VKey.create(vkey.stringify())).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyThenCreate_sqlAndOfyVKey_success() throws Exception {
|
||||
VKey<TestObject> vkey =
|
||||
VKey.create(TestObject.class, "foo", Key.create(TestObject.create("foo")));
|
||||
assertThat(VKey.create(vkey.stringify())).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyThenCreate_asymmetricVKey_success() throws Exception {
|
||||
VKey<TestObject> vkey =
|
||||
VKey.create(TestObject.class, "sqlKey", Key.create(TestObject.create("foo")));
|
||||
assertThat(VKey.create(vkey.stringify())).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyThenCreate_symmetricVKey_success() throws Exception {
|
||||
VKey<TestObject> vkey = TestObject.create("foo").key();
|
||||
assertThat(VKey.create(vkey.stringify())).isEqualTo(vkey);
|
||||
}
|
||||
|
||||
@Entity
|
||||
static class OtherObject {}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ ext {
|
|||
'com.googlecode.json-simple:json-simple:1.1.1',
|
||||
'com.ibm.icu:icu4j:68.2',
|
||||
'com.jcraft:jsch:0.1.55',
|
||||
'commons-codec:commons-codec:1.15',
|
||||
'com.squareup:javapoet:1.13.0',
|
||||
'com.sun.activation:javax.activation:1.2.0',
|
||||
'com.sun.xml.bind:jaxb-impl:2.3.3',
|
||||
|
|
|
@ -31,6 +31,7 @@ dependencies {
|
|||
compile deps['com.google.protobuf:protobuf-java']
|
||||
compile deps['com.google.re2j:re2j']
|
||||
compile deps['com.ibm.icu:icu4j']
|
||||
compile deps['commons-codec:commons-codec']
|
||||
compile deps['javax.inject:javax.inject']
|
||||
compile deps['javax.mail:mail']
|
||||
compile deps['javax.xml.bind:jaxb-api']
|
||||
|
|
|
@ -22,7 +22,9 @@ import java.io.ByteArrayOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
/** Utilities for easy serialization with informative error messages. */
|
||||
public final class SerializeUtils {
|
||||
|
@ -47,7 +49,7 @@ public final class SerializeUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Turns a byte array into an object.
|
||||
* Turns a byte string into an object.
|
||||
*
|
||||
* @return deserialized object or {@code null} if {@code objectBytes} is {@code null}
|
||||
*/
|
||||
|
@ -71,4 +73,19 @@ public final class SerializeUtils {
|
|||
}
|
||||
|
||||
private SerializeUtils() {}
|
||||
|
||||
/** Turns an object into an encoded string that can be used safely as a URI query parameter. */
|
||||
public static String stringify(Serializable object) {
|
||||
checkNotNull(object, "Object cannot be null");
|
||||
return Base64.encodeBase64URLSafeString(SerializeUtils.serialize(object));
|
||||
}
|
||||
|
||||
/** Turns a string encoded by stringify() into an object. */
|
||||
@Nullable
|
||||
public static <T> T parse(Class<T> type, String objectString) {
|
||||
checkNotNull(type, "Class type is not specified");
|
||||
checkNotNull(objectString, "Object string cannot be null");
|
||||
|
||||
return SerializeUtils.deserialize(type, Base64.decodeBase64(objectString));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,12 @@ package google.registry.util;
|
|||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.util.SerializeUtils.deserialize;
|
||||
import static google.registry.util.SerializeUtils.parse;
|
||||
import static google.registry.util.SerializeUtils.serialize;
|
||||
import static google.registry.util.SerializeUtils.stringify;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.Serializable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link SerializeUtils}. */
|
||||
|
@ -61,4 +64,51 @@ class SerializeUtilsTest {
|
|||
() -> deserialize(String.class, new byte[] {(byte) 0xff}));
|
||||
assertThat(thrown).hasMessageThat().contains("Unable to deserialize: objectBytes=FF");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringify_string_returnsBase64EncodedString() {
|
||||
assertThat(stringify("foo")).isEqualTo("rO0ABXQAA2Zvbw");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParse_stringClass_returnsObject() {
|
||||
assertThat(parse(String.class, "rO0ABXQAA2Zvbw")).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyParse_stringValue_maintainsValue() {
|
||||
assertThat(parse(Serializable.class, stringify("hello"))).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringifyParse_longValue_maintainsValue() {
|
||||
assertThat(parse(Serializable.class, stringify((long) 12345))).isEqualTo((long) 12345);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringify_nullValue_throwsException() {
|
||||
NullPointerException thrown = assertThrows(NullPointerException.class, () -> stringify(null));
|
||||
assertThat(thrown).hasMessageThat().contains("Object cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParse_nullClass_throwsException() {
|
||||
NullPointerException thrown =
|
||||
assertThrows(NullPointerException.class, () -> parse(null, "test"));
|
||||
assertThat(thrown).hasMessageThat().contains("Class type is not specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParse_invalidBase64String_throwsException() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> parse(String.class, "abcde:atest"));
|
||||
assertThat(thrown).hasMessageThat().contains("Unable to deserialize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParse_nullObjectStringValue_throwsException() {
|
||||
NullPointerException thrown =
|
||||
assertThrows(NullPointerException.class, () -> parse(String.class, null));
|
||||
assertThat(thrown).hasMessageThat().contains("Object string cannot be null");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue