diff --git a/java/google/registry/rde/RdeImportUtils.java b/java/google/registry/rde/RdeImportUtils.java
new file mode 100644
index 000000000..f3ce5dc23
--- /dev/null
+++ b/java/google/registry/rde/RdeImportUtils.java
@@ -0,0 +1,101 @@
+// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
+//
+// 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.
+
+// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
+//
+// 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 google.registry.rde;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.googlecode.objectify.Key;
+import com.googlecode.objectify.Work;
+import google.registry.model.contact.ContactResource;
+import google.registry.model.index.EppResourceIndex;
+import google.registry.model.index.ForeignKeyIndex;
+import google.registry.model.ofy.Ofy;
+import google.registry.util.Clock;
+import google.registry.util.FormattingLogger;
+import javax.inject.Inject;
+
+/** Utility functions for escrow file import. */
+public final class RdeImportUtils {
+
+ private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
+
+ private final Ofy ofy;
+ private final Clock clock;
+
+ @Inject
+ public RdeImportUtils(Ofy ofy, Clock clock) {
+ this.ofy = ofy;
+ this.clock = clock;
+ }
+
+ /**
+ * Imports a contact from an escrow file.
+ *
+ *
The contact will only be imported if it has not been previously imported.
+ *
+ *
If the contact is imported, {@link ForeignKeyIndex} and {@link EppResourceIndex} are also
+ * created.
+ *
+ * @return true if the contact was created or updated, false otherwise.
+ */
+ public boolean importContact(final ContactResource resource) {
+ return ofy.transact(
+ new Work() {
+ @Override
+ public Boolean run() {
+ ContactResource existing = ofy.load().key(Key.create(resource)).now();
+ if (existing == null) {
+ ForeignKeyIndex existingForeignKeyIndex =
+ ForeignKeyIndex.load(
+ ContactResource.class, resource.getContactId(), clock.nowUtc());
+ // foreign key index should not exist, since existing contact was not found.
+ checkState(
+ existingForeignKeyIndex == null,
+ String.format(
+ "New contact resource has existing foreign key index. "
+ + "contactId=%s, repoId=%s",
+ resource.getContactId(), resource.getRepoId()));
+ ofy.save().entity(resource);
+ ofy.save().entity(ForeignKeyIndex.create(resource, resource.getDeletionTime()));
+ ofy.save().entity(EppResourceIndex.create(Key.create(resource)));
+ logger.infofmt(
+ "Imported contact resource - ROID=%s, id=%s",
+ resource.getRepoId(), resource.getContactId());
+ return true;
+ } else if (!existing.getRepoId().equals(resource.getRepoId())) {
+ logger.warningfmt(
+ "Existing contact with same contact id but different ROID. "
+ + "contactId=%s, existing ROID=%s, new ROID=%s",
+ resource.getContactId(), existing.getRepoId(), resource.getRepoId());
+ }
+ return false;
+ }
+ });
+ }
+}
diff --git a/javatests/google/registry/rde/RdeImportUtilsTest.java b/javatests/google/registry/rde/RdeImportUtilsTest.java
new file mode 100644
index 000000000..29bbc2c06
--- /dev/null
+++ b/javatests/google/registry/rde/RdeImportUtilsTest.java
@@ -0,0 +1,153 @@
+// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
+//
+// 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.
+
+// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
+//
+// 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 google.registry.rde;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.model.ofy.ObjectifyService.ofy;
+import static google.registry.testing.DatastoreHelper.persistResource;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.googlecode.objectify.Key;
+import com.googlecode.objectify.Work;
+import google.registry.model.EppResource;
+import google.registry.model.contact.ContactResource;
+import google.registry.model.index.EppResourceIndex;
+import google.registry.model.index.EppResourceIndexBucket;
+import google.registry.model.index.ForeignKeyIndex;
+import google.registry.testing.AppEngineRule;
+import google.registry.testing.FakeClock;
+import google.registry.testing.ShardableTestCase;
+import org.joda.time.DateTime;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link RdeImportUtils} */
+@RunWith(JUnit4.class)
+public class RdeImportUtilsTest extends ShardableTestCase {
+
+ @Rule
+ public final AppEngineRule appEngine = AppEngineRule.builder()
+ .withDatastore()
+ .build();
+
+ private RdeImportUtils rdeImportUtils;
+ private FakeClock clock;
+
+ @Before
+ public void before() {
+ clock = new FakeClock();
+ clock.setTo(DateTime.now());
+ rdeImportUtils = new RdeImportUtils(ofy(), clock);
+ }
+
+ /** Verifies import of a contact that has not been previously imported */
+ @Test
+ public void testImportNewContact() {
+ ContactResource newContact = buildNewContact();
+ assertThat(rdeImportUtils.importContact(newContact)).isTrue();
+ assertEppResourceIndexEntityFor(newContact);
+ assertForeignKeyIndexFor(newContact);
+
+ // verify the new contact was saved
+ ContactResource saved = getContact("TEST-123");
+ assertThat(saved).isNotNull();
+ assertThat(saved.getContactId()).isEqualTo(newContact.getContactId());
+ assertThat(saved.getEmailAddress()).isEqualTo(newContact.getEmailAddress());
+ assertThat(saved.getLastEppUpdateTime()).isEqualTo(newContact.getLastEppUpdateTime());
+ }
+
+ /** Verifies that a contact will not be imported more than once */
+ @Test
+ public void testImportExistingContact() {
+ ContactResource newContact = buildNewContact();
+ persistResource(newContact);
+ ContactResource updatedContact = newContact.asBuilder()
+ .setLastEppUpdateTime(newContact.getLastEppUpdateTime().plusSeconds(1))
+ .build();
+ assertThat(rdeImportUtils.importContact(updatedContact)).isFalse();
+
+ // verify the updated contact was saved
+ ContactResource saved = getContact("TEST-123");
+ assertThat(saved).isNotNull();
+ assertThat(saved.getContactId()).isEqualTo(newContact.getContactId());
+ assertThat(saved.getEmailAddress()).isEqualTo(newContact.getEmailAddress());
+ assertThat(saved.getLastEppUpdateTime()).isEqualTo(newContact.getLastEppUpdateTime());
+ }
+
+ private static ContactResource buildNewContact() {
+ return new ContactResource.Builder()
+ .setContactId("sh8013")
+ .setEmailAddress("jdoe@example.com")
+ .setLastEppUpdateTime(DateTime.parse("2010-10-10T00:00:00.000Z"))
+ .setRepoId("TEST-123")
+ .build();
+ }
+
+ private static ContactResource getContact(String repoId) {
+ final Key key = Key.create(ContactResource.class, repoId);
+ return ofy().transact(new Work() {
+
+ @Override
+ public ContactResource run() {
+ return ofy().load().key(key).now();
+ }});
+ }
+
+ /**
+ * Confirms that a ForeignKeyIndex exists in the datastore for a given resource.
+ */
+ private static void assertForeignKeyIndexFor(final T resource) {
+ assertThat(ForeignKeyIndex.load(resource.getClass(), resource.getForeignKey(), DateTime.now()))
+ .isNotNull();
+ }
+
+ /**
+ * Confirms that an EppResourceIndex entity exists in datastore for a given resource.
+ */
+ private static void assertEppResourceIndexEntityFor(final T resource) {
+ ImmutableList indices = FluentIterable
+ .from(ofy().load()
+ .type(EppResourceIndex.class)
+ .filter("kind", Key.getKind(resource.getClass())))
+ .filter(new Predicate() {
+ @Override
+ public boolean apply(EppResourceIndex index) {
+ return index.getReference().get().equals(resource);
+ }})
+ .toList();
+ assertThat(indices).hasSize(1);
+ assertThat(indices.get(0).getBucket())
+ .isEqualTo(EppResourceIndexBucket.getBucketKey(Key.create(resource)));
+ }
+}