mirror of
https://github.com/google/nomulus
synced 2026-05-18 05:41:51 +00:00
Compare commits
56 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00941ddc3d | ||
|
|
7b2b49dd53 | ||
|
|
bc8df8f34e | ||
|
|
74b4424509 | ||
|
|
486bf32353 | ||
|
|
b2a78b5d68 | ||
|
|
95f4ae0e3a | ||
|
|
915405b735 | ||
|
|
d7fb6097ba | ||
|
|
d224d96924 | ||
|
|
6d110c77ac | ||
|
|
a79940e822 | ||
|
|
d8ec6294c3 | ||
|
|
9304e2f421 | ||
|
|
5f2be914a1 | ||
|
|
4ad7f9734d | ||
|
|
90d080d42f | ||
|
|
e95ce30fa6 | ||
|
|
090c233592 | ||
|
|
16a31e460c | ||
|
|
a02b67caf5 | ||
|
|
bf20a8ef96 | ||
|
|
8750c07fef | ||
|
|
7821de67f8 | ||
|
|
1022817384 | ||
|
|
8c04bf2599 | ||
|
|
34116e3811 | ||
|
|
d180ef43ac | ||
|
|
f55270c46f | ||
|
|
d6d9874da1 | ||
|
|
e0d04cec4f | ||
|
|
0ce431212e | ||
|
|
32868b3ab8 | ||
|
|
0ecc20b48c | ||
|
|
c65af4b480 | ||
|
|
3a15a8bdc7 | ||
|
|
9806fab880 | ||
|
|
6591e0672a | ||
|
|
91b7d92cf8 | ||
|
|
33910613da | ||
|
|
1fde678250 | ||
|
|
8d56577653 | ||
|
|
3891d411de | ||
|
|
cadecb15d8 | ||
|
|
9b7f6ce500 | ||
|
|
cd23748fe8 | ||
|
|
cf41f5d354 | ||
|
|
9a5ba249db | ||
|
|
f5186f8476 | ||
|
|
4e0ca19d2e | ||
|
|
c812807ab3 | ||
|
|
9edb43f3e4 | ||
|
|
b721533759 | ||
|
|
ce35f6bc93 | ||
|
|
f7a67b7676 | ||
|
|
4438944900 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -97,8 +97,9 @@ nomulus.iws
|
||||
######################################################################
|
||||
# Gradle Ignores
|
||||
|
||||
# We don't want to ignore the gradle jar files
|
||||
# We don't want to ignore the gradle or google-java-format jar files
|
||||
!/gradle/wrapper/**/*.jar
|
||||
!/java-format/*.jar
|
||||
.gradle/
|
||||
**/build
|
||||
cloudbuild-caches/
|
||||
|
||||
@@ -2,3 +2,5 @@ extraction:
|
||||
java:
|
||||
prepare:
|
||||
packages: "npm"
|
||||
index:
|
||||
java_version: "11"
|
||||
|
||||
26
build.gradle
26
build.gradle
@@ -35,9 +35,6 @@ plugins {
|
||||
// Java static analysis plugins. Keep versions consistent with
|
||||
// ./buildSrc/build.gradle
|
||||
id 'nebula.lint' version '16.0.2'
|
||||
// TODO(weiminyu): consider remove net.ltgt.apt. Gradle 5.2+
|
||||
// has similar functionalities.
|
||||
id 'net.ltgt.apt' version '0.19' apply false
|
||||
id 'net.ltgt.errorprone' version '0.6.1'
|
||||
id 'checkstyle'
|
||||
id 'com.github.johnrengelman.shadow' version '5.1.0'
|
||||
@@ -290,12 +287,6 @@ subprojects {
|
||||
|
||||
project.tasks.test.dependsOn runPresubmits
|
||||
|
||||
// Path to code generated with annotation processors. Note that this path is
|
||||
// chosen by the 'net.ltgt.apt' plugin, and may change if IDE-specific plugins
|
||||
// are applied, e.g., 'idea' or 'eclipse'
|
||||
def aptGeneratedDir = "${project.buildDir}/generated/source/apt/main"
|
||||
def aptGeneratedTestDir = "${project.buildDir}/generated/source/apt/test"
|
||||
|
||||
def commonlyExcludedResources = ['**/*.java', '**/BUILD']
|
||||
|
||||
project.ext.javaDir = "${project.projectDir}/src/main/java"
|
||||
@@ -304,18 +295,12 @@ subprojects {
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDirs += aptGeneratedDir
|
||||
}
|
||||
resources {
|
||||
srcDirs += project.ext.javaDir
|
||||
exclude commonlyExcludedResources
|
||||
}
|
||||
}
|
||||
test {
|
||||
java {
|
||||
srcDirs += aptGeneratedTestDir
|
||||
}
|
||||
resources {
|
||||
srcDirs += project.ext.javaTestDir
|
||||
exclude commonlyExcludedResources
|
||||
@@ -323,9 +308,14 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
javadocSource << project.sourceSets.main.allJava
|
||||
javadocClasspath << project.sourceSets.main.compileClasspath
|
||||
javadocDependentTasks << project.tasks.compileJava
|
||||
// No need to produce javadoc for the docs subproject, which has no APIs to
|
||||
// expose to users.
|
||||
if (project.name != 'docs') {
|
||||
javadocSource << project.sourceSets.main.allJava
|
||||
javadocClasspath << project.sourceSets.main.compileClasspath
|
||||
javadocClasspath << "${buildDir}/generated/sources/annotationProcessor/java/main"
|
||||
javadocDependentTasks << project.tasks.compileJava
|
||||
}
|
||||
}
|
||||
|
||||
// If "-P verboseTestOutput=true" is passed in, configure all subprojects to dump all of their
|
||||
|
||||
@@ -26,9 +26,6 @@ buildscript {
|
||||
plugins {
|
||||
// Java static analysis plugins. Keep versions consistent with ../build.gradle
|
||||
id 'nebula.lint' version '16.0.2'
|
||||
// Config helper for annotation processors such as AutoValue and Dagger.
|
||||
// Ensures that source code is generated at an appropriate location.
|
||||
id 'net.ltgt.apt' version '0.19' apply false
|
||||
id 'net.ltgt.errorprone' version '0.6.1'
|
||||
id 'checkstyle'
|
||||
id 'com.diffplug.gradle.spotless' version '3.25.0'
|
||||
@@ -83,10 +80,8 @@ dependencies {
|
||||
annotationProcessor deps['com.google.auto.value:auto-value']
|
||||
testCompile deps['com.google.truth:truth']
|
||||
testCompile deps['com.google.truth.extensions:truth-java8-extension']
|
||||
testCompile deps['junit:junit']
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-api']
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-engine']
|
||||
testCompile deps['org.junit.vintage:junit-vintage-engine']
|
||||
testCompile deps['org.mockito:mockito-core']
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ commons-lang:commons-lang:2.6
|
||||
javax.inject:javax.inject:1
|
||||
junit:junit:4.12
|
||||
nebula.lint:nebula.lint.gradle.plugin:16.0.2
|
||||
net.ltgt.apt:net.ltgt.apt.gradle.plugin:0.19
|
||||
net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:0.6.1
|
||||
net.ltgt.gradle:gradle-apt-plugin:0.19
|
||||
net.ltgt.gradle:gradle-errorprone-plugin:0.6.1
|
||||
org.apache.commons:commons-lang3:3.8.1
|
||||
org.apache.maven:maven-artifact:3.6.2
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.google.errorprone:javac:9+181-r4173-1
|
||||
@@ -49,7 +49,7 @@ javax.annotation:jsr250-api:1.0
|
||||
javax.inject:javax.inject:1
|
||||
javax.validation:validation-api:1.0.0.GA
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
net.bytebuddy:byte-buddy-agent:1.10.5
|
||||
net.bytebuddy:byte-buddy:1.10.5
|
||||
org.apache.commons:commons-lang3:3.8.1
|
||||
@@ -65,7 +65,6 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.mockito:mockito-core:3.3.3
|
||||
org.objenesis:objenesis:2.6
|
||||
|
||||
@@ -49,7 +49,7 @@ javax.annotation:jsr250-api:1.0
|
||||
javax.inject:javax.inject:1
|
||||
javax.validation:validation-api:1.0.0.GA
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
net.bytebuddy:byte-buddy-agent:1.10.5
|
||||
net.bytebuddy:byte-buddy:1.10.5
|
||||
org.apache.commons:commons-lang3:3.8.1
|
||||
@@ -65,7 +65,6 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.mockito:mockito-core:3.3.3
|
||||
org.objenesis:objenesis:2.6
|
||||
|
||||
@@ -49,7 +49,7 @@ javax.annotation:jsr250-api:1.0
|
||||
javax.inject:javax.inject:1
|
||||
javax.validation:validation-api:1.0.0.GA
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
net.bytebuddy:byte-buddy-agent:1.10.5
|
||||
net.bytebuddy:byte-buddy:1.10.5
|
||||
org.apache.commons:commons-lang3:3.8.1
|
||||
@@ -65,7 +65,6 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.mockito:mockito-core:3.3.3
|
||||
org.objenesis:objenesis:2.6
|
||||
|
||||
@@ -49,7 +49,7 @@ javax.annotation:jsr250-api:1.0
|
||||
javax.inject:javax.inject:1
|
||||
javax.validation:validation-api:1.0.0.GA
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
net.bytebuddy:byte-buddy-agent:1.10.5
|
||||
net.bytebuddy:byte-buddy:1.10.5
|
||||
org.apache.commons:commons-lang3:3.8.1
|
||||
@@ -65,7 +65,6 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.mockito:mockito-core:3.3.3
|
||||
org.objenesis:objenesis:2.6
|
||||
|
||||
@@ -57,6 +57,10 @@ final class GcsPluginUtils {
|
||||
return file.toPath().toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
static Path toNormalizedPath(Path file) {
|
||||
return file.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
static String getContentType(String fileName) {
|
||||
return EXTENSION_TO_CONTENT_TYPE.getOrDefault(
|
||||
Files.getFileExtension(fileName), DEFAULT_CONTENT_TYPE);
|
||||
|
||||
@@ -26,13 +26,10 @@ import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.gradle.plugin.ProjectData.TaskData;
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Tests for {@link CoverPageGenerator} */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class CoverPageGeneratorTest {
|
||||
final class CoverPageGeneratorTest {
|
||||
|
||||
private static final ProjectData EMPTY_PROJECT =
|
||||
ProjectData.builder()
|
||||
@@ -88,14 +85,14 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilesToUpload_entryPoint_isIndexHtml() {
|
||||
void testGetFilesToUpload_entryPoint_isIndexHtml() {
|
||||
CoverPageGenerator coverPageGenerator = new CoverPageGenerator(EMPTY_PROJECT);
|
||||
assertThat(coverPageGenerator.getFilesToUpload().entryPoint())
|
||||
.isEqualTo(Paths.get("index.html"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilesToUpload_containsEntryFile() {
|
||||
void testGetFilesToUpload_containsEntryFile() {
|
||||
String content = getContentOfGeneratedFile(EMPTY_PROJECT, "index.html");
|
||||
assertThat(content)
|
||||
.contains(
|
||||
@@ -103,7 +100,7 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCoverPage_showsFailedTask() {
|
||||
void testCoverPage_showsFailedTask() {
|
||||
String content = getCoverPage(EMPTY_PROJECT.toBuilder().addTask(EMPTY_TASK_FAILURE).build());
|
||||
assertThat(content).contains("task-failure");
|
||||
assertThat(content).contains("<p>FAILURE</p>");
|
||||
@@ -112,7 +109,7 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCoverPage_showsSuccessfulTask() {
|
||||
void testCoverPage_showsSuccessfulTask() {
|
||||
String content = getCoverPage(EMPTY_PROJECT.toBuilder().addTask(EMPTY_TASK_SUCCESS).build());
|
||||
assertThat(content).contains("task-success");
|
||||
assertThat(content).doesNotContain("<p>FAILURE</p>");
|
||||
@@ -121,7 +118,7 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCoverPage_showsUpToDateTask() {
|
||||
void testCoverPage_showsUpToDateTask() {
|
||||
String content = getCoverPage(EMPTY_PROJECT.toBuilder().addTask(EMPTY_TASK_UP_TO_DATE).build());
|
||||
assertThat(content).contains("task-up-to-date");
|
||||
assertThat(content).doesNotContain("<p>FAILURE</p>");
|
||||
@@ -130,11 +127,10 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCoverPage_failedAreFirst() {
|
||||
void testCoverPage_failedAreFirst() {
|
||||
String content =
|
||||
getCoverPage(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_UP_TO_DATE)
|
||||
.addTask(EMPTY_TASK_FAILURE)
|
||||
.addTask(EMPTY_TASK_SUCCESS)
|
||||
@@ -149,11 +145,10 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCoverPage_failingTask_statusIsFailure() {
|
||||
void testCoverPage_failingTask_statusIsFailure() {
|
||||
String content =
|
||||
getCoverPage(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_UP_TO_DATE)
|
||||
.addTask(EMPTY_TASK_FAILURE)
|
||||
.addTask(EMPTY_TASK_SUCCESS)
|
||||
@@ -162,11 +157,10 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCoverPage_noFailingTask_statusIsSuccess() {
|
||||
void testCoverPage_noFailingTask_statusIsSuccess() {
|
||||
String content =
|
||||
getCoverPage(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_UP_TO_DATE)
|
||||
.addTask(EMPTY_TASK_SUCCESS)
|
||||
.build());
|
||||
@@ -174,7 +168,7 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFilesToUpload_containsCssFile() {
|
||||
void testGetFilesToUpload_containsCssFile() {
|
||||
ImmutableMap<String, String> files = getGeneratedFiles(EMPTY_PROJECT);
|
||||
assertThat(files).containsKey(filenameJoiner.join("css", "style.css"));
|
||||
assertThat(files.get(filenameJoiner.join("css", "style.css"))).contains("body {");
|
||||
@@ -183,14 +177,12 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_taskWithLog() {
|
||||
void testCreateReportFiles_taskWithLog() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS
|
||||
.toBuilder()
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.setUniqueName("my:name")
|
||||
.setLog(toByteArraySupplier("my log data"))
|
||||
.build())
|
||||
@@ -200,11 +192,10 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_taskWithoutLog() {
|
||||
void testCreateReportFiles_taskWithoutLog() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(EMPTY_TASK_SUCCESS.toBuilder().setUniqueName("my:name").build())
|
||||
.build());
|
||||
assertThat(files).doesNotContainKey("logs/my-name.log");
|
||||
@@ -212,14 +203,12 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_taskWithFilledReport() {
|
||||
void testCreateReportFiles_taskWithFilledReport() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS
|
||||
.toBuilder()
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.putReport(
|
||||
"someReport",
|
||||
FilesWithEntryPoint.create(
|
||||
@@ -234,14 +223,12 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_taskWithEmptyReport() {
|
||||
void testCreateReportFiles_taskWithEmptyReport() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS
|
||||
.toBuilder()
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.putReport(
|
||||
"someReport",
|
||||
FilesWithEntryPoint.create(
|
||||
@@ -254,14 +241,12 @@ public final class CoverPageGeneratorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_taskWithLogAndMultipleReports() {
|
||||
void testCreateReportFiles_taskWithLogAndMultipleReports() {
|
||||
ImmutableMap<String, String> files =
|
||||
getGeneratedFiles(
|
||||
EMPTY_PROJECT
|
||||
.toBuilder()
|
||||
EMPTY_PROJECT.toBuilder()
|
||||
.addTask(
|
||||
EMPTY_TASK_SUCCESS
|
||||
.toBuilder()
|
||||
EMPTY_TASK_SUCCESS.toBuilder()
|
||||
.setUniqueName("my:name")
|
||||
.setLog(toByteArraySupplier("log data"))
|
||||
.putReport(
|
||||
|
||||
@@ -23,6 +23,9 @@ import static google.registry.gradle.plugin.GcsPluginUtils.toNormalizedPath;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.uploadFileToGcs;
|
||||
import static google.registry.gradle.plugin.GcsPluginUtils.uploadFilesToGcsMultithread;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.Files.createDirectories;
|
||||
import static java.nio.file.Files.createDirectory;
|
||||
import static java.nio.file.Files.createFile;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@@ -36,22 +39,20 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/** Tests for {@link GcsPluginUtilsTest} */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class GcsPluginUtilsTest {
|
||||
final class GcsPluginUtilsTest {
|
||||
|
||||
private static final Joiner filenameJoiner = Joiner.on(File.separator);
|
||||
|
||||
@Rule public final TemporaryFolder folder = new TemporaryFolder();
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@TempDir
|
||||
Path tmpDir;
|
||||
|
||||
@Test
|
||||
public void testGetContentType_knownTypes() {
|
||||
void testGetContentType_knownTypes() {
|
||||
assertThat(getContentType("path/to/file.html")).isEqualTo("text/html");
|
||||
assertThat(getContentType("path/to/file.htm")).isEqualTo("text/html");
|
||||
assertThat(getContentType("path/to/file.log")).isEqualTo("text/plain");
|
||||
@@ -63,12 +64,12 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetContentType_unknownTypes() {
|
||||
void testGetContentType_unknownTypes() {
|
||||
assertThat(getContentType("path/to/file.unknown")).isEqualTo("application/octet-stream");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadFileToGcs() {
|
||||
void testUploadFileToGcs() {
|
||||
Storage storage = mock(Storage.class);
|
||||
uploadFileToGcs(
|
||||
storage, "my-bucket", Paths.get("my", "filename.txt"), toByteArraySupplier("my data"));
|
||||
@@ -82,7 +83,7 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUploadFilesToGcsMultithread() {
|
||||
void testUploadFilesToGcsMultithread() {
|
||||
Storage storage = mock(Storage.class);
|
||||
uploadFilesToGcsMultithread(
|
||||
storage,
|
||||
@@ -121,21 +122,21 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToByteArraySupplier_string() {
|
||||
void testToByteArraySupplier_string() {
|
||||
assertThat(toByteArraySupplier("my string").get()).isEqualTo("my string".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToByteArraySupplier_stringSupplier() {
|
||||
void testToByteArraySupplier_stringSupplier() {
|
||||
assertThat(toByteArraySupplier(() -> "my string").get()).isEqualTo("my string".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToByteArraySupplier_file() throws Exception {
|
||||
folder.newFolder("arbitrary");
|
||||
File file = folder.newFile("arbitrary/file.txt");
|
||||
Files.write(file.toPath(), "some data".getBytes(UTF_8));
|
||||
assertThat(toByteArraySupplier(file).get()).isEqualTo("some data".getBytes(UTF_8));
|
||||
void testToByteArraySupplier_file() throws Exception {
|
||||
Path dir = createDirectory(tmpDir.resolve("arbitrary"));
|
||||
Path file = createFile(dir.resolve("file.txt"));
|
||||
Files.write(file, "some data".getBytes(UTF_8));
|
||||
assertThat(toByteArraySupplier(file.toFile()).get()).isEqualTo("some data".getBytes(UTF_8));
|
||||
}
|
||||
|
||||
private ImmutableMap<String, String> readAllFiles(FilesWithEntryPoint reportFiles) {
|
||||
@@ -147,16 +148,16 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_destinationIsFile() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
folder.newFolder("my", "root", "some", "path");
|
||||
File destination = folder.newFile("my/root/some/path/file.txt");
|
||||
Files.write(destination.toPath(), "some data".getBytes(UTF_8));
|
||||
void testCreateReportFiles_destinationIsFile() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path somePath = createDirectories(root.resolve("some/path"));
|
||||
Path destination = createFile(somePath.resolve("file.txt"));
|
||||
Files.write(destination, "some data".getBytes(UTF_8));
|
||||
// Since the entry point is obvious here - any hint given is just ignored.
|
||||
File ignoredHint = folder.newFile("my/root/ignored.txt");
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(ignoredHint), root);
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "file.txt"));
|
||||
@@ -165,13 +166,13 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_destinationDoesntExist() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
void testCreateReportFiles_destinationDoesntExist() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
File destination = root.resolve("non/existing.txt").toFile();
|
||||
assertThat(destination.isFile()).isFalse();
|
||||
assertThat(destination.isDirectory()).isFalse();
|
||||
// Since there are not files, any hint given is obvioulsy wrong and will be ignored.
|
||||
File ignoredHint = folder.newFile("my/root/ignored.txt");
|
||||
// Since there are no files, any hint given is obviously wrong and will be ignored.
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(ignoredHint), root);
|
||||
@@ -181,34 +182,33 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_noFiles() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
File destination = folder.newFolder("my", "root", "some", "path");
|
||||
folder.newFolder("my", "root", "some", "path", "a", "b");
|
||||
folder.newFolder("my", "root", "some", "path", "c");
|
||||
// Since there are not files, any hint given is obvioulsy wrong and will be ignored.
|
||||
File ignoredHint = folder.newFile("my/root/ignored.txt");
|
||||
void testCreateReportFiles_noFiles() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
// Since there are not files, any hint given is obviously wrong and will be ignored.
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(ignoredHint), root);
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString()).isEqualTo(filenameJoiner.join("some", "path"));
|
||||
assertThat(files.files()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_oneFile() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
File destination = folder.newFolder("my", "root", "some", "path");
|
||||
folder.newFolder("my", "root", "some", "path", "a", "b");
|
||||
folder.newFolder("my", "root", "some", "path", "c");
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/a/file.txt").toPath(), "some data".getBytes(UTF_8));
|
||||
void testCreateReportFiles_oneFile() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
Files.write(createFile(destination.resolve("a/file.txt")), "some data".getBytes(UTF_8));
|
||||
// Since the entry point is obvious here - any hint given is just ignored.
|
||||
File ignoredHint = folder.newFile("my/root/ignored.txt");
|
||||
File ignoredHint = createFile(root.resolve("ignored.txt")).toFile();
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(ignoredHint), root);
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(ignoredHint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "a", "file.txt"));
|
||||
@@ -222,22 +222,19 @@ public final class GcsPluginUtilsTest {
|
||||
* <p>TODO(guyben): switch to checking zip file instead.
|
||||
*/
|
||||
@Test
|
||||
public void testCreateReportFiles_multipleFiles_noHint() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
File destination = folder.newFolder("my", "root", "some", "path");
|
||||
folder.newFolder("my", "root", "some", "path", "a", "b");
|
||||
folder.newFolder("my", "root", "some", "path", "c");
|
||||
void testCreateReportFiles_multipleFiles_noHint() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/index.html").toPath(), "some data".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/a/index.html").toPath(), "wrong index".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/c/style.css").toPath(), "css file".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/my_image.png").toPath(), "images".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("index.html")), "some data".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("a/index.html")), "wrong index".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("c/style.css")), "css file".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("my_image.png")), "images".getBytes(UTF_8));
|
||||
|
||||
FilesWithEntryPoint files = readFilesWithEntryPoint(destination, Optional.empty(), root);
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.empty(), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "path.zip"));
|
||||
@@ -251,24 +248,20 @@ public final class GcsPluginUtilsTest {
|
||||
* <p>TODO(guyben): switch to checking zip file instead.
|
||||
*/
|
||||
@Test
|
||||
public void testCreateReportFiles_multipleFiles_withBadHint() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
File destination = folder.newFolder("my", "root", "some", "path");
|
||||
void testCreateReportFiles_multipleFiles_withBadHint() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
// This entry point points to a directory, which isn't an appropriate entry point
|
||||
File badEntryPoint = folder.newFolder("my", "root", "some", "path", "a", "b");
|
||||
folder.newFolder("my", "root", "some", "path", "c");
|
||||
File badEntryPoint = createDirectories(destination.resolve("a/b")).toFile();
|
||||
createDirectory(destination.resolve("c"));
|
||||
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/index.html").toPath(), "some data".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/a/index.html").toPath(), "wrong index".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/c/style.css").toPath(), "css file".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/my_image.png").toPath(), "images".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("index.html")), "some data".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("a/index.html")), "wrong index".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("c/style.css")), "css file".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("my_image.png")), "images".getBytes(UTF_8));
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(badEntryPoint), root);
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(badEntryPoint), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "path.zip"));
|
||||
@@ -277,24 +270,21 @@ public final class GcsPluginUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateReportFiles_multipleFiles_withGoodHint() throws Exception {
|
||||
Path root = toNormalizedPath(folder.newFolder("my", "root"));
|
||||
File destination = folder.newFolder("my", "root", "some", "path");
|
||||
folder.newFolder("my", "root", "some", "path", "a", "b");
|
||||
folder.newFolder("my", "root", "some", "path", "c");
|
||||
void testCreateReportFiles_multipleFiles_withGoodHint() throws Exception {
|
||||
Path root = toNormalizedPath(createDirectories(tmpDir.resolve("my/root")).toAbsolutePath());
|
||||
Path destination = createDirectories(root.resolve("some/path"));
|
||||
createDirectories(destination.resolve("a/b"));
|
||||
createDirectory(destination.resolve("c"));
|
||||
// The hint is an actual file nested in the destination directory!
|
||||
File goodEntryPoint = folder.newFile("my/root/some/path/index.html");
|
||||
Path goodEntryPoint = createFile(destination.resolve("index.html"));
|
||||
|
||||
Files.write(goodEntryPoint.toPath(), "some data".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/a/index.html").toPath(), "wrong index".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/c/style.css").toPath(), "css file".getBytes(UTF_8));
|
||||
Files.write(
|
||||
folder.newFile("my/root/some/path/my_image.png").toPath(), "images".getBytes(UTF_8));
|
||||
Files.write(goodEntryPoint, "some data".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("a/index.html")), "wrong index".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("c/style.css")), "css file".getBytes(UTF_8));
|
||||
Files.write(createFile(destination.resolve("my_image.png")), "images".getBytes(UTF_8));
|
||||
|
||||
FilesWithEntryPoint files =
|
||||
readFilesWithEntryPoint(destination, Optional.of(goodEntryPoint), root);
|
||||
readFilesWithEntryPoint(destination.toFile(), Optional.of(goodEntryPoint.toFile()), root);
|
||||
|
||||
assertThat(files.entryPoint().toString())
|
||||
.isEqualTo(filenameJoiner.join("some", "path", "index.html"));
|
||||
|
||||
@@ -61,8 +61,6 @@ dependencies {
|
||||
testingCompile deps['com.google.truth:truth']
|
||||
testingCompile deps['io.github.java-diff-utils:java-diff-utils']
|
||||
|
||||
testCompile deps['junit:junit']
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-api']
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-engine']
|
||||
testCompile deps['org.junit.vintage:junit-vintage-engine']
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.google.errorprone:javac:9+181-r4173-1
|
||||
|
||||
@@ -14,7 +14,7 @@ com.googlecode.java-diff-utils:diffutils:1.3.0
|
||||
io.github.java-diff-utils:java-diff-utils:4.0
|
||||
javax.inject:javax.inject:1
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
org.apiguardian:apiguardian-api:1.1.0
|
||||
org.checkerframework:checker-compat-qual:2.5.5
|
||||
org.checkerframework:checker-qual:2.11.1
|
||||
@@ -24,6 +24,5 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.opentest4j:opentest4j:1.2.0
|
||||
|
||||
@@ -14,7 +14,7 @@ com.googlecode.java-diff-utils:diffutils:1.3.0
|
||||
io.github.java-diff-utils:java-diff-utils:4.0
|
||||
javax.inject:javax.inject:1
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
org.apiguardian:apiguardian-api:1.1.0
|
||||
org.checkerframework:checker-compat-qual:2.5.5
|
||||
org.checkerframework:checker-qual:2.11.1
|
||||
@@ -24,6 +24,5 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.opentest4j:opentest4j:1.2.0
|
||||
|
||||
@@ -15,7 +15,7 @@ com.googlecode.java-diff-utils:diffutils:1.3.0
|
||||
io.github.java-diff-utils:java-diff-utils:4.0
|
||||
javax.inject:javax.inject:1
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
org.apiguardian:apiguardian-api:1.1.0
|
||||
org.checkerframework:checker-compat-qual:2.5.5
|
||||
org.checkerframework:checker-qual:2.11.1
|
||||
@@ -25,6 +25,5 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.opentest4j:opentest4j:1.2.0
|
||||
|
||||
@@ -15,7 +15,7 @@ com.googlecode.java-diff-utils:diffutils:1.3.0
|
||||
io.github.java-diff-utils:java-diff-utils:4.0
|
||||
javax.inject:javax.inject:1
|
||||
joda-time:joda-time:2.9.2
|
||||
junit:junit:4.13
|
||||
junit:junit:4.12
|
||||
org.apiguardian:apiguardian-api:1.1.0
|
||||
org.checkerframework:checker-compat-qual:2.5.5
|
||||
org.checkerframework:checker-qual:2.11.1
|
||||
@@ -25,6 +25,5 @@ org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.platform:junit-platform-commons:1.6.2
|
||||
org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.opentest4j:opentest4j:1.2.0
|
||||
|
||||
@@ -17,7 +17,7 @@ package google.registry.testing.truth;
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.truth.TextDiffSubject.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -25,13 +25,10 @@ import com.google.common.io.Resources;
|
||||
import google.registry.testing.truth.TextDiffSubject.DiffFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link TextDiffSubject}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class TextDiffSubjectTest {
|
||||
class TextDiffSubjectTest {
|
||||
|
||||
private static final String RESOURCE_FOLDER = "google/registry/testing/truth/";
|
||||
// Resources for input data.
|
||||
@@ -44,21 +41,21 @@ public class TextDiffSubjectTest {
|
||||
RESOURCE_FOLDER + "text-sidebyside-diff.txt";
|
||||
|
||||
@Test
|
||||
public void unifiedDiff_equal() throws IOException {
|
||||
void unifiedDiff_equal() throws IOException {
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.UNIFIED_DIFF)
|
||||
.hasSameContentAs(getResource(ACTUAL_RESOURCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sideBySideDiff_equal() throws IOException {
|
||||
void sideBySideDiff_equal() throws IOException {
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN)
|
||||
.hasSameContentAs(getResource(ACTUAL_RESOURCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unifedDiff_notEqual() throws IOException {
|
||||
void unifedDiff_notEqual() throws IOException {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
@@ -68,7 +65,7 @@ public class TextDiffSubjectTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sideBySideDiff_notEqual() throws IOException {
|
||||
void sideBySideDiff_notEqual() throws IOException {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
@@ -78,13 +75,13 @@ public class TextDiffSubjectTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayed_unifiedDiff_noDiff() throws IOException {
|
||||
void displayed_unifiedDiff_noDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
assertThat(TextDiffSubject.generateUnifiedDiff(actual, actual)).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayed_unifiedDiff_hasDiff() throws IOException {
|
||||
void displayed_unifiedDiff_hasDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
ImmutableList<String> expected = readAllLinesFromResource(EXPECTED_RESOURCE);
|
||||
String diff = Joiner.on('\n').join(readAllLinesFromResource(UNIFIED_DIFF_RESOURCE));
|
||||
@@ -92,7 +89,7 @@ public class TextDiffSubjectTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayed_sideBySideDiff_hasDiff() throws IOException {
|
||||
void displayed_sideBySideDiff_hasDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
ImmutableList<String> expected = readAllLinesFromResource(EXPECTED_RESOURCE);
|
||||
String diff = Joiner.on('\n').join(readAllLinesFromResource(SIDE_BY_SIDE_DIFF_RESOURCE));
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.testing.truth;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.github.difflib.DiffUtils;
|
||||
@@ -31,6 +32,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.truth.Fact;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.SimpleSubjectBuilder;
|
||||
import com.google.common.truth.Subject;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -68,6 +70,15 @@ public class TextDiffSubject extends Subject {
|
||||
this.actual = ImmutableList.copyOf(actual);
|
||||
}
|
||||
|
||||
protected TextDiffSubject(FailureMetadata metadata, URL actual) {
|
||||
super(metadata, actual);
|
||||
try {
|
||||
this.actual = ImmutableList.copyOf(Resources.asCharSource(actual, UTF_8).readLines());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public TextDiffSubject withDiffFormat(DiffFormat format) {
|
||||
this.diffFormat = format;
|
||||
return this;
|
||||
@@ -100,6 +111,11 @@ public class TextDiffSubject extends Subject {
|
||||
return assertThat(Resources.asCharSource(resourceUrl, UTF_8).readLines());
|
||||
}
|
||||
|
||||
public static SimpleSubjectBuilder<TextDiffSubject, URL> assertWithMessageAboutUrlSource(
|
||||
String format, Object... params) {
|
||||
return assertWithMessage(format, params).about(urlFactory());
|
||||
}
|
||||
|
||||
private static final Subject.Factory<TextDiffSubject, ImmutableList<String>>
|
||||
TEXT_DIFF_SUBJECT_TEXT_FACTORY = TextDiffSubject::new;
|
||||
|
||||
@@ -107,6 +123,13 @@ public class TextDiffSubject extends Subject {
|
||||
return TEXT_DIFF_SUBJECT_TEXT_FACTORY;
|
||||
}
|
||||
|
||||
private static final Subject.Factory<TextDiffSubject, URL> TEXT_DIFF_SUBJECT_URL_FACTORY =
|
||||
TextDiffSubject::new;
|
||||
|
||||
public static Subject.Factory<TextDiffSubject, URL> urlFactory() {
|
||||
return TEXT_DIFF_SUBJECT_URL_FACTORY;
|
||||
}
|
||||
|
||||
static String generateUnifiedDiff(
|
||||
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||
Patch<String> diff;
|
||||
|
||||
@@ -19,6 +19,7 @@ import argparse
|
||||
import attr
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import List, Union
|
||||
@@ -49,15 +50,30 @@ PROPERTIES_HEADER = """\
|
||||
# This file defines properties used by the gradle build. It must be kept in
|
||||
# sync with config/nom_build.py.
|
||||
#
|
||||
# To regenerate, run config/nom_build.py --generate-gradle-properties
|
||||
# To regenerate, run ./nom_build --generate-gradle-properties
|
||||
#
|
||||
# To view property descriptions (which are command line flags for
|
||||
# nom_build), run config/nom_build.py --help.
|
||||
# nom_build), run ./nom_build --help.
|
||||
#
|
||||
# DO NOT EDIT THIS FILE BY HAND
|
||||
org.gradle.jvmargs=-Xmx1024m
|
||||
"""
|
||||
|
||||
# Help text to be displayed (in addition to the synopsis and flag help, which
|
||||
# are displayed automatically).
|
||||
HELP_TEXT = """\
|
||||
A wrapper around the gradle build that provides the following features:
|
||||
|
||||
- Converts properties into flags to guard against property name spelling errors
|
||||
and to provide help descriptions for all properties.
|
||||
- Provides pseudo-commands (with the ":nom:" prefix) that encapsulate common
|
||||
actions that are difficult to implement in gradle.
|
||||
|
||||
Pseudo-commands:
|
||||
:nom:generate_golden_file - regenerates the golden file from the current
|
||||
set of flyway files.
|
||||
"""
|
||||
|
||||
# Define all of our special gradle properties here.
|
||||
PROPERTIES = [
|
||||
Property('mavenUrl',
|
||||
@@ -114,6 +130,11 @@ PROPERTIES = [
|
||||
Property('nomulus_version',
|
||||
'The version of nomulus to test against in a database '
|
||||
'integration test.'),
|
||||
Property('dot_path',
|
||||
'The path to "dot", part of the graphviz package that converts '
|
||||
'a BEAM pipeline to image. Setting this property to empty string '
|
||||
'will disable image generation.',
|
||||
'/usr/bin/dot'),
|
||||
]
|
||||
|
||||
GRADLE_FLAGS = [
|
||||
@@ -251,8 +272,42 @@ def get_root() -> str:
|
||||
return cur_dir
|
||||
|
||||
|
||||
def main(args):
|
||||
parser = argparse.ArgumentParser('nom_build')
|
||||
class Abort(Exception):
|
||||
"""Raised to terminate the process with a non-zero error code.
|
||||
|
||||
Parameters are ignored.
|
||||
"""
|
||||
|
||||
|
||||
def do_pseudo_task(task: str) -> None:
|
||||
root = get_root()
|
||||
if task == ':nom:generate_golden_file':
|
||||
if not subprocess.call([f'{root}/gradlew', ':db:test']):
|
||||
print('\033[33mWARNING:\033[0m Golden schema appears to be '
|
||||
'up-to-date. If you are making schema changes, be sure to '
|
||||
'add a flyway file for them.')
|
||||
return
|
||||
print('\033[33mWARNING:\033[0m Ignore the above failure, it is '
|
||||
'expected.')
|
||||
|
||||
# Copy the new schema into place.
|
||||
shutil.copy(f'{root}/db/build/resources/test/testcontainer/'
|
||||
'mount/dump.txt',
|
||||
f'{root}/db/src/main/resources/sql/schema/'
|
||||
'nomulus.golden.sql')
|
||||
|
||||
if subprocess.call([f'{root}/gradlew', ':db:test']):
|
||||
print('\033[31mERROR:\033[0m Golden file test failed after '
|
||||
'copying schema. Please check your flyway files.')
|
||||
raise Abort()
|
||||
else:
|
||||
print(f'\033[31mERROR:\033[0m Unknown task {task}')
|
||||
raise Abort()
|
||||
|
||||
|
||||
def main(args) -> int:
|
||||
parser = argparse.ArgumentParser('nom_build', description=HELP_TEXT,
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
for prop in PROPERTIES:
|
||||
parser.add_argument('--' + prop.name, default=prop.default,
|
||||
help=prop.desc)
|
||||
@@ -291,7 +346,7 @@ def main(args):
|
||||
if args.generate_gradle_properties:
|
||||
with open(f'{root}/gradle.properties', 'w') as dst:
|
||||
dst.write(gradle_properties)
|
||||
return
|
||||
return 0
|
||||
|
||||
# Verify that the gradle properties file is what we expect it to be.
|
||||
with open(f'{root}/gradle.properties') as src:
|
||||
@@ -316,12 +371,39 @@ def main(args):
|
||||
if flag.has_arg:
|
||||
gradle_command.append(arg_val)
|
||||
|
||||
# See if there are any special ":nom:" pseudo-tasks specified.
|
||||
got_non_pseudo_tasks = False
|
||||
for arg in args.non_flag_args[1:]:
|
||||
if arg.startswith(':nom:'):
|
||||
if got_non_pseudo_tasks:
|
||||
# We can't currently deal with the situation of gradle tasks
|
||||
# before pseudo-tasks. This could be implemented by invoking
|
||||
# gradle for only the set of gradle tasks before the pseudo
|
||||
# task, but that's overkill for now.
|
||||
print(f'\033[31mERROR:\033[0m Pseudo task ({arg}) must be '
|
||||
'specified prior to all actual gradle tasks. Aborting.')
|
||||
return 1
|
||||
do_pseudo_task(arg)
|
||||
else:
|
||||
got_non_pseudo_tasks = True
|
||||
non_flag_args = [
|
||||
arg for arg in args.non_flag_args[1:] if not arg.startswith(':nom:')]
|
||||
|
||||
if not non_flag_args:
|
||||
if not got_non_pseudo_tasks:
|
||||
print('\033[33mWARNING:\033[0m No tasks specified. Not '
|
||||
'doing anything')
|
||||
return 0
|
||||
|
||||
# Add the non-flag args (we exclude the first, which is the command name
|
||||
# itself) and run.
|
||||
gradle_command.extend(args.non_flag_args[1:])
|
||||
subprocess.call(gradle_command)
|
||||
gradle_command.extend(non_flag_args)
|
||||
return subprocess.call(gradle_command)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
try:
|
||||
sys.exit(main(sys.argv))
|
||||
except Abort as ex:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import nom_build
|
||||
@@ -67,6 +68,7 @@ class MyTest(unittest.TestCase):
|
||||
mock.patch.object(nom_build, 'print', self.print_fake).start())
|
||||
|
||||
self.call_mock = mock.patch.object(subprocess, 'call').start()
|
||||
self.copy_mock = mock.patch.object(shutil, 'copy').start()
|
||||
|
||||
self.file_contents = {
|
||||
# Prefil with the actual file contents.
|
||||
@@ -92,17 +94,32 @@ class MyTest(unittest.TestCase):
|
||||
|
||||
def test_no_args(self):
|
||||
nom_build.main(['nom_build'])
|
||||
self.assertEqual(self.printed, [])
|
||||
self.call_mock.assert_called_with([GRADLEW])
|
||||
self.assertEqual(self.printed,
|
||||
['\x1b[33mWARNING:\x1b[0m No tasks specified. Not '
|
||||
'doing anything'])
|
||||
|
||||
def test_property_calls(self):
|
||||
nom_build.main(['nom_build', '--testFilter=foo'])
|
||||
self.call_mock.assert_called_with([GRADLEW, '-P', 'testFilter=foo'])
|
||||
nom_build.main(['nom_build', 'task-name', '--testFilter=foo'])
|
||||
self.call_mock.assert_called_with([GRADLEW, '-P', 'testFilter=foo',
|
||||
'task-name'])
|
||||
|
||||
def test_gradle_flags(self):
|
||||
nom_build.main(['nom_build', '-d', '-b', 'foo'])
|
||||
nom_build.main(['nom_build', 'task-name', '-d', '-b', 'foo'])
|
||||
self.call_mock.assert_called_with([GRADLEW, '--build-file', 'foo',
|
||||
'--debug'])
|
||||
'--debug', 'task-name'])
|
||||
|
||||
def test_generate_golden_file(self):
|
||||
self.call_mock.side_effect = [1, 0]
|
||||
nom_build.main(['nom_build', ':nom:generate_golden_file'])
|
||||
self.call_mock.assert_has_calls([
|
||||
mock.call([GRADLEW, ':db:test']),
|
||||
mock.call([GRADLEW, ':db:test'])
|
||||
])
|
||||
|
||||
def test_generate_golden_file_nofail(self):
|
||||
self.call_mock.return_value = 0
|
||||
nom_build.main(['nom_build', ':nom:generate_golden_file'])
|
||||
self.call_mock.assert_has_calls([mock.call([GRADLEW, ':db:test'])])
|
||||
|
||||
unittest.main()
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import sys
|
||||
import re
|
||||
|
||||
# We should never analyze any generated files
|
||||
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/"}
|
||||
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/"}
|
||||
# We can't rely on CI to have the Enum package installed so we do this instead.
|
||||
FORBIDDEN = 1
|
||||
REQUIRED = 2
|
||||
@@ -93,20 +93,20 @@ PRESUBMITS = {
|
||||
PresubmitCheck(
|
||||
r".*\bSystem\.(out|err)\.print", "java", {
|
||||
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
||||
"RegistryTestServerMain.java", "TestServerRule.java",
|
||||
"RegistryTestServerMain.java", "TestServerExtension.java",
|
||||
"FlowDocumentationTool.java"
|
||||
}):
|
||||
"System.(out|err).println is only allowed in tools/ packages. Please "
|
||||
"use a logger instead.",
|
||||
|
||||
# ObjectifyService.register is restricted to main/ or AppEngineRule.
|
||||
# ObjectifyService.register is restricted to main/ or AppEngineExtension.
|
||||
PresubmitCheck(
|
||||
r".*\bObjectifyService\.register", "java", {
|
||||
"/build/", "/generated/", "node_modules/", "src/main/",
|
||||
"AppEngineRule.java"
|
||||
"AppEngineExtension.java"
|
||||
}):
|
||||
"ObjectifyService.register is not allowed in tests. Please use "
|
||||
"AppengineRule.register instead.",
|
||||
"ObjectifyService.register(...) is not allowed in tests. Please use "
|
||||
"AppEngineExtension.register(...) instead.",
|
||||
|
||||
# PostgreSQLContainer instantiation must specify docker tag
|
||||
PresubmitCheck(
|
||||
|
||||
@@ -22,7 +22,7 @@ plugins {
|
||||
|
||||
// Path to code generated by ad hoc tasks in this project. A separate path is
|
||||
// used for easy inspection.
|
||||
def generatedDir = "${project.buildDir}/generated/source/custom/main"
|
||||
def generatedDir = "${project.buildDir}/generated/sources/custom/java/main"
|
||||
def resourcesDir = "${project.buildDir}/resources/main"
|
||||
def screenshotsDir = "${project.buildDir}/screenshots"
|
||||
def screenshotsForGoldensDir = "${project.buildDir}/screenshots_for_goldens"
|
||||
@@ -63,6 +63,8 @@ def dockerIncompatibleTestPatterns = [
|
||||
// methods, so we exclude the whole test class.
|
||||
"google/registry/tools/params/PathParameterTest.*",
|
||||
"google/registry/persistence/PersistenceModuleTest.*",
|
||||
// This test is failing in docker when using Java 11. The cause is unclear.
|
||||
"google/registry/tools/DomainLockUtilsTest.*",
|
||||
]
|
||||
|
||||
// Tests that conflict with members of both the main test suite and the
|
||||
@@ -82,11 +84,6 @@ sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDirs += generatedDir
|
||||
// Javadoc API is deprecated in Java 11 and removed in Java 12.
|
||||
// TODO(jianglai): re-enable after migrating to the new Javadoc API
|
||||
if ((JavaVersion.current().majorVersion as Integer) >= 11) {
|
||||
exclude 'google/registry/documentation/**'
|
||||
}
|
||||
}
|
||||
resources {
|
||||
exclude '**/*.xjb'
|
||||
@@ -238,6 +235,7 @@ dependencies {
|
||||
compile deps['jline:jline']
|
||||
compile deps['joda-time:joda-time']
|
||||
compile deps['org.apache.avro:avro']
|
||||
testCompile deps['org.apache.beam:beam-runners-core-construction-java']
|
||||
testCompile deps['org.apache.beam:beam-runners-direct-java']
|
||||
compile deps['org.apache.beam:beam-runners-google-cloud-dataflow-java']
|
||||
compile deps['org.apache.beam:beam-sdks-java-core']
|
||||
@@ -256,12 +254,13 @@ dependencies {
|
||||
compile deps['org.bouncycastle:bcpg-jdk15on']
|
||||
testCompile deps['org.bouncycastle:bcpkix-jdk15on']
|
||||
compile deps['org.bouncycastle:bcprov-jdk15on']
|
||||
testCompile deps['com.fasterxml.jackson.core:jackson-databind']
|
||||
runtime deps['org.glassfish.jaxb:jaxb-runtime']
|
||||
compile deps['org.hibernate:hibernate-core']
|
||||
compile deps['org.joda:joda-money']
|
||||
compile deps['org.json:json']
|
||||
testCompile deps['org.mortbay.jetty:jetty']
|
||||
runtimeOnly deps['org.postgresql:postgresql']
|
||||
compile deps['org.postgresql:postgresql']
|
||||
testCompile deps['org.seleniumhq.selenium:selenium-api']
|
||||
testCompile deps['org.seleniumhq.selenium:selenium-chrome-driver']
|
||||
testCompile deps['org.seleniumhq.selenium:selenium-java']
|
||||
@@ -272,7 +271,6 @@ dependencies {
|
||||
compile deps['org.testcontainers:postgresql']
|
||||
testCompile deps['org.testcontainers:selenium']
|
||||
testCompile deps['org.testcontainers:testcontainers']
|
||||
testCompile deps['pl.pragmatists:JUnitParams']
|
||||
compile deps['xerces:xmlParserAPIs']
|
||||
compile deps['xpp3:xpp3']
|
||||
// This dependency must come after javax.mail:mail as it would otherwise
|
||||
@@ -313,9 +311,9 @@ dependencies {
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-engine']
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-migrationsupport']
|
||||
testCompile deps['org.junit.jupiter:junit-jupiter-params']
|
||||
testCompile deps['org.junit-pioneer:junit-pioneer']
|
||||
testCompile deps['org.junit.platform:junit-platform-runner']
|
||||
testCompile deps['org.junit.platform:junit-platform-suite-api']
|
||||
testCompile deps['org.junit.vintage:junit-vintage-engine']
|
||||
testCompile deps['org.mockito:mockito-core']
|
||||
testCompile deps['org.mockito:mockito-junit-jupiter']
|
||||
runtime deps['org.postgresql:postgresql']
|
||||
@@ -408,6 +406,9 @@ task jaxbToJava {
|
||||
}
|
||||
}
|
||||
}
|
||||
execInBash(
|
||||
'find . -name *.java -exec sed -i /\\*\\ \\<p\\>\\$/d {} +',
|
||||
generatedDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,6 +707,10 @@ task fragileTest(type: FilteringTest) {
|
||||
|
||||
// Run every test class in a freshly started process.
|
||||
forkEvery 1
|
||||
|
||||
doFirst {
|
||||
new File(screenshotsDir).deleteDir()
|
||||
}
|
||||
}
|
||||
|
||||
task outcastTest(type: FilteringTest) {
|
||||
@@ -823,6 +828,30 @@ createToolTask(
|
||||
'google.registry.tools.DevTool',
|
||||
sourceSets.nonprod)
|
||||
|
||||
project.tasks.create('initSqlPipeline', JavaExec) {
|
||||
main = 'google.registry.beam.initsql.InitSqlPipeline'
|
||||
|
||||
doFirst {
|
||||
getToolArgsList().ifPresent {
|
||||
args it
|
||||
}
|
||||
|
||||
def isDirectRunner =
|
||||
args.contains('DirectRunner') || args.contains('--runner=DirectRunner')
|
||||
// The dependency containing DirectRunner is intentionally excluded from the
|
||||
// production binary, so that it won't be chosen by mistake: we definitely do
|
||||
// not want to use it for the real jobs, yet DirectRunner is the default if
|
||||
// the user forgets to override it.
|
||||
// DirectRunner is required for tests and is already on testRuntimeClasspath.
|
||||
// For simplicity, we add testRuntimeClasspath to this task's classpath instead
|
||||
// of defining a new configuration just for the DirectRunner dependency.
|
||||
classpath =
|
||||
isDirectRunner
|
||||
? sourceSets.main.runtimeClasspath.plus(sourceSets.test.runtimeClasspath)
|
||||
: sourceSets.main.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks.create('generateSqlSchema', JavaExec) {
|
||||
classpath = sourceSets.nonprod.runtimeClasspath
|
||||
main = 'google.registry.tools.DevTool'
|
||||
@@ -850,22 +879,6 @@ task generateGoldenImages(type: FilteringTest) {
|
||||
}
|
||||
generateGoldenImages.finalizedBy(findGoldenImages)
|
||||
|
||||
task flowDocsTool(type: JavaExec) {
|
||||
systemProperty 'test.projectRoot', rootProject.projectRootDir
|
||||
systemProperty 'test.resourcesDir', resourcesDir
|
||||
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'google.registry.documentation.FlowDocumentationTool'
|
||||
|
||||
def arguments = []
|
||||
if (rootProject.flowDocsFile) {
|
||||
arguments << "--output_file=${rootProject.flowDocsFile}"
|
||||
} else {
|
||||
arguments << "--output_file=${rootProject.projectRootDir}/docs/flows.md"
|
||||
}
|
||||
args arguments
|
||||
}
|
||||
|
||||
task standardTest(type: FilteringTest) {
|
||||
includeAllTests()
|
||||
exclude fragileTestPatterns
|
||||
@@ -890,10 +903,6 @@ task standardTest(type: FilteringTest) {
|
||||
|
||||
systemProperty 'test.projectRoot', rootProject.projectRootDir
|
||||
systemProperty 'test.resourcesDir', resourcesDir
|
||||
|
||||
doFirst {
|
||||
new File(screenshotsDir).deleteDir()
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -967,6 +976,49 @@ task buildToolImage(dependsOn: nomulus, type: Exec) {
|
||||
commandLine 'docker', 'build', '-t', 'nomulus-tool', '.'
|
||||
}
|
||||
|
||||
task generateInitSqlPipelineGraph(type: Test) {
|
||||
include "**/InitSqlPipelineGraphTest.*"
|
||||
testNameIncludePatterns = ["**createPipeline_compareGraph"]
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
task updateInitSqlPipelineGraph(type: Copy) {
|
||||
def graphRelativePath = 'google/registry/beam/initsql/'
|
||||
from ("${projectDir}/build/resources/test/${graphRelativePath}") {
|
||||
include 'pipeline_curr.dot'
|
||||
rename 'curr', 'golden'
|
||||
}
|
||||
into "src/test/resources/${graphRelativePath}"
|
||||
|
||||
dependsOn generateInitSqlPipelineGraph
|
||||
|
||||
doLast {
|
||||
if (com.google.common.base.Strings.isNullOrEmpty(project.dot_path)) {
|
||||
getLogger().info('Property dot_path is null. Not creating image for pipeline graph.')
|
||||
}
|
||||
def dotPath = project.dot_path
|
||||
if (!new File(dotPath).exists()) {
|
||||
throw new RuntimeException(
|
||||
"""\
|
||||
${dotPath} not found. Make sure graphviz is installed
|
||||
and the dot_path property is set correctly."""
|
||||
.stripIndent())
|
||||
}
|
||||
def goldenGraph = "src/test/resources/${graphRelativePath}/pipeline_golden.dot"
|
||||
def goldenImage = "src/test/resources/${graphRelativePath}/pipeline_golden.png"
|
||||
def cmd = "${dotPath} -Tpng -o \"${goldenImage}\" \"${goldenGraph}\""
|
||||
try {
|
||||
rootProject.ext.execInBash(cmd, projectDir)
|
||||
} catch (Throwable throwable) {
|
||||
throw new RuntimeException(
|
||||
"""\
|
||||
Failed to generate golden image with command ${cmd}
|
||||
Error: ${throwable.getMessage()}
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the devtool jar.
|
||||
createUberJar(
|
||||
'devtool',
|
||||
|
||||
@@ -228,6 +228,7 @@ org.ow2.asm:asm-commons:7.1
|
||||
org.ow2.asm:asm-tree:8.0.1
|
||||
org.ow2.asm:asm-util:8.0.1
|
||||
org.ow2.asm:asm:8.0.1
|
||||
org.postgresql:postgresql:42.2.14
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.rnorth:tcp-unix-socket-proxy:1.0.2
|
||||
|
||||
@@ -223,6 +223,7 @@ org.ow2.asm:asm-commons:7.1
|
||||
org.ow2.asm:asm-tree:8.0.1
|
||||
org.ow2.asm:asm-util:8.0.1
|
||||
org.ow2.asm:asm:8.0.1
|
||||
org.postgresql:postgresql:42.2.14
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.rnorth:tcp-unix-socket-proxy:1.0.2
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
com.google.errorprone:javac:9+181-r4173-1
|
||||
|
||||
@@ -228,6 +228,7 @@ org.ow2.asm:asm-commons:7.1
|
||||
org.ow2.asm:asm-tree:8.0.1
|
||||
org.ow2.asm:asm-util:8.0.1
|
||||
org.ow2.asm:asm:8.0.1
|
||||
org.postgresql:postgresql:42.2.14
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.rnorth:tcp-unix-socket-proxy:1.0.2
|
||||
|
||||
@@ -226,6 +226,7 @@ org.ow2.asm:asm-commons:7.1
|
||||
org.ow2.asm:asm-tree:8.0.1
|
||||
org.ow2.asm:asm-util:8.0.1
|
||||
org.ow2.asm:asm:8.0.1
|
||||
org.postgresql:postgresql:42.2.14
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.rnorth:tcp-unix-socket-proxy:1.0.2
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
@@ -243,6 +243,7 @@ org.jboss:jandex:2.1.3.Final
|
||||
org.jetbrains:annotations:19.0.0
|
||||
org.joda:joda-money:1.0.1
|
||||
org.json:json:20160810
|
||||
org.junit-pioneer:junit-pioneer:0.7.0
|
||||
org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-migrationsupport:5.6.2
|
||||
@@ -252,7 +253,6 @@ org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.platform:junit-platform-launcher:1.6.2
|
||||
org.junit.platform:junit-platform-runner:1.6.2
|
||||
org.junit.platform:junit-platform-suite-api:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.jvnet.staxex:stax-ex:1.8
|
||||
org.mockito:mockito-core:3.3.3
|
||||
@@ -266,6 +266,7 @@ org.ow2.asm:asm-commons:7.1
|
||||
org.ow2.asm:asm-tree:8.0.1
|
||||
org.ow2.asm:asm-util:8.0.1
|
||||
org.ow2.asm:asm:8.0.1
|
||||
org.postgresql:postgresql:42.2.14
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.rnorth:tcp-unix-socket-proxy:1.0.2
|
||||
@@ -292,6 +293,5 @@ org.tukaani:xz:1.8
|
||||
org.w3c.css:sac:1.3
|
||||
org.xerial.snappy:snappy-java:1.1.4
|
||||
org.yaml:snakeyaml:1.17
|
||||
pl.pragmatists:JUnitParams:1.1.1
|
||||
xerces:xmlParserAPIs:2.6.2
|
||||
xpp3:xpp3:1.1.4c
|
||||
|
||||
@@ -241,6 +241,7 @@ org.jboss:jandex:2.1.3.Final
|
||||
org.jetbrains:annotations:19.0.0
|
||||
org.joda:joda-money:1.0.1
|
||||
org.json:json:20160810
|
||||
org.junit-pioneer:junit-pioneer:0.7.0
|
||||
org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-migrationsupport:5.6.2
|
||||
@@ -250,7 +251,6 @@ org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.platform:junit-platform-launcher:1.6.2
|
||||
org.junit.platform:junit-platform-runner:1.6.2
|
||||
org.junit.platform:junit-platform-suite-api:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.jvnet.staxex:stax-ex:1.8
|
||||
org.mockito:mockito-core:3.3.3
|
||||
@@ -264,6 +264,7 @@ org.ow2.asm:asm-commons:7.1
|
||||
org.ow2.asm:asm-tree:8.0.1
|
||||
org.ow2.asm:asm-util:8.0.1
|
||||
org.ow2.asm:asm:8.0.1
|
||||
org.postgresql:postgresql:42.2.14
|
||||
org.rnorth.duct-tape:duct-tape:1.0.8
|
||||
org.rnorth.visible-assertions:visible-assertions:2.1.2
|
||||
org.rnorth:tcp-unix-socket-proxy:1.0.2
|
||||
@@ -290,6 +291,5 @@ org.tukaani:xz:1.8
|
||||
org.w3c.css:sac:1.3
|
||||
org.xerial.snappy:snappy-java:1.1.4
|
||||
org.yaml:snakeyaml:1.17
|
||||
pl.pragmatists:JUnitParams:1.1.1
|
||||
xerces:xmlParserAPIs:2.6.2
|
||||
xpp3:xpp3:1.1.4c
|
||||
|
||||
@@ -246,6 +246,7 @@ org.jboss:jandex:2.1.3.Final
|
||||
org.jetbrains:annotations:19.0.0
|
||||
org.joda:joda-money:1.0.1
|
||||
org.json:json:20160810
|
||||
org.junit-pioneer:junit-pioneer:0.7.0
|
||||
org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-migrationsupport:5.6.2
|
||||
@@ -255,7 +256,6 @@ org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.platform:junit-platform-launcher:1.6.2
|
||||
org.junit.platform:junit-platform-runner:1.6.2
|
||||
org.junit.platform:junit-platform-suite-api:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.jvnet.staxex:stax-ex:1.8
|
||||
org.mockito:mockito-core:3.3.3
|
||||
@@ -296,6 +296,5 @@ org.tukaani:xz:1.8
|
||||
org.w3c.css:sac:1.3
|
||||
org.xerial.snappy:snappy-java:1.1.4
|
||||
org.yaml:snakeyaml:1.17
|
||||
pl.pragmatists:JUnitParams:1.1.1
|
||||
xerces:xmlParserAPIs:2.6.2
|
||||
xpp3:xpp3:1.1.4c
|
||||
|
||||
@@ -246,6 +246,7 @@ org.jboss:jandex:2.1.3.Final
|
||||
org.jetbrains:annotations:19.0.0
|
||||
org.joda:joda-money:1.0.1
|
||||
org.json:json:20160810
|
||||
org.junit-pioneer:junit-pioneer:0.7.0
|
||||
org.junit.jupiter:junit-jupiter-api:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-engine:5.6.2
|
||||
org.junit.jupiter:junit-jupiter-migrationsupport:5.6.2
|
||||
@@ -255,7 +256,6 @@ org.junit.platform:junit-platform-engine:1.6.2
|
||||
org.junit.platform:junit-platform-launcher:1.6.2
|
||||
org.junit.platform:junit-platform-runner:1.6.2
|
||||
org.junit.platform:junit-platform-suite-api:1.6.2
|
||||
org.junit.vintage:junit-vintage-engine:5.6.2
|
||||
org.junit:junit-bom:5.6.2
|
||||
org.jvnet.staxex:stax-ex:1.8
|
||||
org.mockito:mockito-core:3.3.3
|
||||
@@ -297,6 +297,5 @@ org.tukaani:xz:1.8
|
||||
org.w3c.css:sac:1.3
|
||||
org.xerial.snappy:snappy-java:1.1.4
|
||||
org.yaml:snakeyaml:1.17
|
||||
pl.pragmatists:JUnitParams:1.1.1
|
||||
xerces:xmlParserAPIs:2.6.2
|
||||
xpp3:xpp3:1.1.4c
|
||||
|
||||
@@ -39,7 +39,7 @@ module.exports = function(config) {
|
||||
included: false
|
||||
},
|
||||
{
|
||||
pattern: 'core/build/generated/source/custom/main/**/*.soy.js',
|
||||
pattern: 'core/build/generated/sources/custom/java/main/**/*.soy.js',
|
||||
included: false
|
||||
},
|
||||
{
|
||||
@@ -65,7 +65,7 @@ module.exports = function(config) {
|
||||
'node_modules/google-closure-library/closure/goog/base.js': ['closure'],
|
||||
'node_modules/google-closure-library/closure/**/*.js': ['closure'],
|
||||
'core/src/*/javascript/**/*.js': ['closure'],
|
||||
'core/build/generated/source/custom/main/**/*.soy.js': ['closure'],
|
||||
'core/build/generated/sources/custom/java/main/**/*.soy.js': ['closure'],
|
||||
},
|
||||
proxies: {
|
||||
"/assets/": "/base/core/build/resources/main/google/registry/ui/assets/"
|
||||
|
||||
@@ -22,21 +22,35 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* Sets up a placeholder {@link Environment} on a non-AppEngine platform so that Datastore Entities
|
||||
* can be converted from/to Objectify entities. See {@code DatastoreEntityExtension} in test source
|
||||
* for more information.
|
||||
* Sets up a fake {@link Environment} so that the following operations can be performed without the
|
||||
* Datastore service:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Create Objectify {@code Keys}.
|
||||
* <li>Instantiate Objectify objects.
|
||||
* <li>Convert Datastore {@code Entities} to their corresponding Objectify objects.
|
||||
* </ul>
|
||||
*
|
||||
* <p>User has the option to specify their desired {@code appId} string, which forms part of an
|
||||
* Objectify {@code Key} and is included in the equality check. This feature makes it easy to
|
||||
* compare a migrated object in SQL with the original in Objectify.
|
||||
*
|
||||
* <p>Note that conversion from Objectify objects to Datastore {@code Entities} still requires the
|
||||
* Datastore service.
|
||||
*/
|
||||
public class AppEngineEnvironment implements Closeable {
|
||||
|
||||
private static final Environment PLACEHOLDER_ENV = createAppEngineEnvironment();
|
||||
|
||||
private boolean isPlaceHolderNeeded;
|
||||
|
||||
public AppEngineEnvironment() {
|
||||
this("PlaceholderAppId");
|
||||
}
|
||||
|
||||
public AppEngineEnvironment(String appId) {
|
||||
isPlaceHolderNeeded = ApiProxy.getCurrentEnvironment() == null;
|
||||
// isPlaceHolderNeeded may be true when we are invoked in a test with AppEngineRule.
|
||||
if (isPlaceHolderNeeded) {
|
||||
ApiProxy.setEnvironmentForCurrentThread(PLACEHOLDER_ENV);
|
||||
ApiProxy.setEnvironmentForCurrentThread(createAppEngineEnvironment(appId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +62,7 @@ public class AppEngineEnvironment implements Closeable {
|
||||
}
|
||||
|
||||
/** Returns a placeholder {@link Environment} that can return hardcoded AppId and Attributes. */
|
||||
private static Environment createAppEngineEnvironment() {
|
||||
private static Environment createAppEngineEnvironment(String appId) {
|
||||
return (Environment)
|
||||
Proxy.newProxyInstance(
|
||||
Environment.class.getClassLoader(),
|
||||
@@ -56,7 +70,7 @@ public class AppEngineEnvironment implements Closeable {
|
||||
(Object proxy, Method method, Object[] args) -> {
|
||||
switch (method.getName()) {
|
||||
case "getAppId":
|
||||
return "PlaceholderAppId";
|
||||
return appId;
|
||||
case "getAttributes":
|
||||
return ImmutableMap.<String, Object>of();
|
||||
default:
|
||||
|
||||
@@ -169,10 +169,12 @@ public final class AsyncTaskEnqueuer {
|
||||
lock.getRelockDuration().isPresent(),
|
||||
"Lock with ID %s not configured for relock",
|
||||
lock.getRevisionId());
|
||||
String backendHostname = appEngineServiceUtils.getServiceHostname("backend");
|
||||
addTaskToQueueWithRetry(
|
||||
asyncActionsPushQueue,
|
||||
TaskOptions.Builder.withUrl(RelockDomainAction.PATH)
|
||||
.method(Method.POST)
|
||||
.header("Host", backendHostname)
|
||||
.param(
|
||||
RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM,
|
||||
String.valueOf(lock.getRevisionId()))
|
||||
|
||||
@@ -32,7 +32,7 @@ public final class BackupPaths {
|
||||
private BackupPaths() {}
|
||||
|
||||
private static final String WILDCARD_CHAR = "*";
|
||||
private static final String EXPORT_PATTERN_TEMPLATE = "%s/all_namespaces/kind_%s/input-%s";
|
||||
private static final String EXPORT_PATTERN_TEMPLATE = "%s/all_namespaces/kind_%s/output-%s";
|
||||
|
||||
public static final String COMMIT_LOG_NAME_PREFIX = "commit_diff_until_";
|
||||
private static final String COMMIT_LOG_PATTERN_TEMPLATE = "%s/" + COMMIT_LOG_NAME_PREFIX + "*";
|
||||
|
||||
@@ -54,37 +54,39 @@ public class BeamJpaModule {
|
||||
|
||||
private static final String GCS_SCHEME = "gs://";
|
||||
|
||||
@Nullable private final String credentialFilePath;
|
||||
@Nullable private final String sqlAccessInfoFile;
|
||||
@Nullable private final String cloudKmsProjectId;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@link BeamJpaModule}.
|
||||
*
|
||||
* <p>Note: it is an unfortunately necessary antipattern to check for the validity of
|
||||
* credentialFilePath in {@link #provideCloudSqlAccessInfo} rather than in the constructor.
|
||||
* sqlAccessInfoFile in {@link #provideCloudSqlAccessInfo} rather than in the constructor.
|
||||
* Unfortunately, this is a restriction imposed upon us by Dagger. Specifically, because we use
|
||||
* this in at least one 1 {@link google.registry.tools.RegistryTool} command(s), it must be
|
||||
* instantiated in {@code google.registry.tools.RegistryToolComponent} for all possible commands;
|
||||
* Dagger doesn't permit it to ever be null. For the vast majority of commands, it will never be
|
||||
* used (so a null credential file path is fine in those cases).
|
||||
*
|
||||
* @param credentialFilePath the path to a Cloud SQL credential file. This must refer to either a
|
||||
* @param sqlAccessInfoFile the path to a Cloud SQL credential file. This must refer to either a
|
||||
* real encrypted file on GCS as returned by {@link
|
||||
* BackupPaths#getCloudSQLCredentialFilePatterns} or an unencrypted file on local filesystem
|
||||
* with credentials to a test database.
|
||||
*/
|
||||
public BeamJpaModule(@Nullable String credentialFilePath) {
|
||||
this.credentialFilePath = credentialFilePath;
|
||||
public BeamJpaModule(@Nullable String sqlAccessInfoFile, @Nullable String cloudKmsProjectId) {
|
||||
this.sqlAccessInfoFile = sqlAccessInfoFile;
|
||||
this.cloudKmsProjectId = cloudKmsProjectId;
|
||||
}
|
||||
|
||||
/** Returns true if the credential file is on GCS (and therefore expected to be encrypted). */
|
||||
private boolean isCloudSqlCredential() {
|
||||
return credentialFilePath.startsWith(GCS_SCHEME);
|
||||
return sqlAccessInfoFile.startsWith(GCS_SCHEME);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
SqlAccessInfo provideCloudSqlAccessInfo(Lazy<CloudSqlCredentialDecryptor> lazyDecryptor) {
|
||||
checkArgument(!isNullOrEmpty(credentialFilePath), "Null or empty credentialFilePath");
|
||||
checkArgument(!isNullOrEmpty(sqlAccessInfoFile), "Null or empty credentialFilePath");
|
||||
String line = readOnlyLineFromCredentialFile();
|
||||
if (isCloudSqlCredential()) {
|
||||
line = lazyDecryptor.get().decrypt(line);
|
||||
@@ -101,7 +103,7 @@ public class BeamJpaModule {
|
||||
|
||||
String readOnlyLineFromCredentialFile() {
|
||||
try {
|
||||
ResourceId resourceId = FileSystems.matchSingleFileSpec(credentialFilePath).resourceId();
|
||||
ResourceId resourceId = FileSystems.matchSingleFileSpec(sqlAccessInfoFile).resourceId();
|
||||
try (BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
@@ -141,8 +143,8 @@ public class BeamJpaModule {
|
||||
|
||||
@Provides
|
||||
@Config("beamCloudKmsProjectId")
|
||||
static String kmsProjectId() {
|
||||
return "domain-registry-dev";
|
||||
String kmsProjectId() {
|
||||
return cloudKmsProjectId;
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2020 The Nomulus 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.beam.initsql;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.appengine.api.datastore.Entity;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Helper for manipulating {@code DomainBase} when migrating from Datastore to SQL database */
|
||||
final class DomainBaseUtil {
|
||||
|
||||
private DomainBaseUtil() {}
|
||||
|
||||
/**
|
||||
* Removes {@link google.registry.model.billing.BillingEvent.Recurring}, {@link
|
||||
* google.registry.model.poll.PollMessage PollMessages} and {@link
|
||||
* google.registry.model.host.HostResource name servers} from a Datastore {@link Entity} that
|
||||
* represents an Ofy {@link google.registry.model.domain.DomainBase}. This breaks the cycle of
|
||||
* foreign key constraints between these entity kinds, allowing {@code DomainBases} to be inserted
|
||||
* into the SQL database. See {@link InitSqlPipeline} for a use case, where the full {@code
|
||||
* DomainBases} are written again during the last stage of the pipeline.
|
||||
*
|
||||
* <p>The returned object may be in bad state. Specifically, {@link
|
||||
* google.registry.model.eppcommon.StatusValue#INACTIVE} is not added after name servers are
|
||||
* removed. This only impacts tests.
|
||||
*
|
||||
* <p>This operation is performed on an Datastore {@link Entity} instead of Ofy Java object
|
||||
* because Objectify requires access to a Datastore service when converting an Ofy object to a
|
||||
* Datastore {@code Entity}. If we insist on working with Objectify objects, we face a few
|
||||
* unsatisfactory options:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Connect to our production Datastore, which incurs unnecessary security and code health
|
||||
* risk.
|
||||
* <li>Connect to a separate real Datastore instance, which is a waster and overkill.
|
||||
* <li>Use an in-memory test Datastore, which is a project health risk in that the test
|
||||
* Datastore would be added to Nomulus' production binary unless we create a separate
|
||||
* project for this pipeline.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Given our use case, operating on Datastore entities is the best option.
|
||||
*
|
||||
* @throws IllegalArgumentException if input does not represent a DomainBase
|
||||
*/
|
||||
static Entity removeBillingAndPollAndHosts(Entity domainBase) {
|
||||
checkNotNull(domainBase, "domainBase");
|
||||
checkArgument(
|
||||
Objects.equals(domainBase.getKind(), "DomainBase"),
|
||||
"Expecting DomainBase, got %s",
|
||||
domainBase.getKind());
|
||||
Entity clone = domainBase.clone();
|
||||
clone.removeProperty("autorenewBillingEvent");
|
||||
clone.removeProperty("autorenewPollMessage");
|
||||
clone.removeProperty("deletePollMessage");
|
||||
clone.removeProperty("nsHosts");
|
||||
domainBase.getProperties().keySet().stream()
|
||||
.filter(s -> s.startsWith("transferData."))
|
||||
.forEach(s -> clone.removeProperty(s));
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright 2020 The Nomulus 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.beam.initsql;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.backup.AppEngineEnvironment;
|
||||
import google.registry.backup.VersionedEntity;
|
||||
import google.registry.beam.initsql.BeamJpaModule.JpaTransactionManagerComponent;
|
||||
import google.registry.beam.initsql.Transforms.RemoveDomainBaseForeignKeys;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarContact;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import org.apache.beam.sdk.Pipeline;
|
||||
import org.apache.beam.sdk.PipelineResult;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.transforms.ParDo;
|
||||
import org.apache.beam.sdk.transforms.SerializableFunction;
|
||||
import org.apache.beam.sdk.transforms.Wait;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.apache.beam.sdk.values.PCollectionTuple;
|
||||
import org.apache.beam.sdk.values.TupleTag;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* A BEAM pipeline that populates a SQL database with data from a Datastore backup.
|
||||
*
|
||||
* <p>This pipeline migrates EPP resources and related entities that cross-reference each other. To
|
||||
* avoid violating foreign key constraints, writes to SQL are ordered by entity kinds. In addition,
|
||||
* the {@link DomainBase} kind is written twice (see details below). The write order is presented
|
||||
* below. Although some kinds can be written concurrently, e.g. {@code ContactResource} and {@code
|
||||
* RegistrarContact}, we do not expect any performance benefit since the limiting resource is the
|
||||
* number of JDBC connections. Google internal users may refer to <a
|
||||
* href="http://go/registry-r3-init-sql">the design doc</a> for more information.
|
||||
*
|
||||
* <ol>
|
||||
* <li>{@link Registry}: Assumes that {@code PremiumList} and {@code ReservedList} have been set
|
||||
* up in the SQL database.
|
||||
* <li>{@link Registrar}: Logically depends on {@code Registry}, Foreign key not modeled yet.
|
||||
* <li>{@link ContactResource}: references {@code Registrar}
|
||||
* <li>{@link RegistrarContact}: references {@code Registrar}.
|
||||
* <li>Cleansed {@link DomainBase}: with references to {@code BillingEvent}, {@code Recurring},
|
||||
* {@code Cancellation} and {@code HostResource} removed, still references {@code Registrar}
|
||||
* and {@code ContactResource}. The removal breaks circular Foreign Key references.
|
||||
* <li>{@link HostResource}: references {@code DomainBase}.
|
||||
* <li>{@link HistoryEntry}: maps to one of three SQL entity types and may reference {@code
|
||||
* Registrar}, {@code ContactResource}, {@code HostResource}, and {@code DomainBase}.
|
||||
* <li>{@link AllocationToken}: references {@code HistoryEntry}.
|
||||
* <li>{@link BillingEvent.Recurring}: references {@code Registrar}, {@code DomainBase} and {@code
|
||||
* HistoryEntry}.
|
||||
* <li>{@link BillingEvent.OneTime}: references {@code Registrar}, {@code DomainBase}, {@code
|
||||
* BillingEvent.Recurring}, {@code HistoryEntry} and {@code AllocationToken}.
|
||||
* <li>{@link BillingEvent.Modification}: SQL model TBD. Will reference {@code Registrar}, {@code
|
||||
* DomainBase} and {@code BillingEvent.OneTime}.
|
||||
* <li>{@link BillingEvent.Cancellation}: references {@code Registrar}, {@code DomainBase}, {@code
|
||||
* BillingEvent.Recurring}, {@code BillingEvent.OneTime}, and {@code HistoryEntry}.
|
||||
* <li>{@link PollMessage}: references {@code Registrar}, {@code DomainBase}, {@code
|
||||
* ContactResource}, {@code HostResource}, and {@code HistoryEntry}.
|
||||
* <li>{@link DomainBase}, original copy from Datastore.
|
||||
* </ol>
|
||||
*/
|
||||
public class InitSqlPipeline implements Serializable {
|
||||
|
||||
/**
|
||||
* Datastore kinds to be written to the SQL database before the cleansed version of {@link
|
||||
* DomainBase}.
|
||||
*/
|
||||
// TODO(weiminyu): include Registry.class when it is modeled in JPA.
|
||||
private static final ImmutableList<Class<?>> PHASE_ONE_ORDERED =
|
||||
ImmutableList.of(Registrar.class, ContactResource.class);
|
||||
|
||||
/**
|
||||
* Datastore kinds to be written to the SQL database after the cleansed version of {@link
|
||||
* DomainBase}.
|
||||
*
|
||||
* <p>The following entities are missing from the list:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Those not modeled in JPA yet, e.g., {@code BillingEvent.Modification}.
|
||||
* <li>Those waiting for sanitation, e.g., {@code HistoryEntry}, which would have duplicate keys
|
||||
* after converting to SQL model.
|
||||
* <li>Those that have foreign key constraints on the above.
|
||||
* </ul>
|
||||
*/
|
||||
// TODO(weiminyu): add more entities when available.
|
||||
private static final ImmutableList<Class<?>> PHASE_TWO_ORDERED =
|
||||
ImmutableList.of(HostResource.class);
|
||||
|
||||
private final InitSqlPipelineOptions options;
|
||||
|
||||
private final Pipeline pipeline;
|
||||
|
||||
private final SerializableFunction<JpaTransactionManagerComponent, JpaTransactionManager>
|
||||
jpaGetter;
|
||||
|
||||
InitSqlPipeline(InitSqlPipelineOptions options) {
|
||||
this.options = options;
|
||||
pipeline = Pipeline.create(options);
|
||||
jpaGetter = JpaTransactionManagerComponent::cloudSqlJpaTransactionManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
InitSqlPipeline(InitSqlPipelineOptions options, Pipeline pipeline) {
|
||||
this.options = options;
|
||||
this.pipeline = pipeline;
|
||||
jpaGetter = JpaTransactionManagerComponent::localDbJpaTransactionManager;
|
||||
}
|
||||
|
||||
public PipelineResult run() {
|
||||
setupPipeline();
|
||||
return pipeline.run();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setupPipeline() {
|
||||
PCollectionTuple datastoreSnapshot =
|
||||
pipeline.apply(
|
||||
"Load Datastore snapshot",
|
||||
Transforms.loadDatastoreSnapshot(
|
||||
options.getDatastoreExportDir(),
|
||||
options.getCommitLogDir(),
|
||||
DateTime.parse(options.getCommitLogStartTimestamp()),
|
||||
DateTime.parse(options.getCommitLogEndTimestamp()),
|
||||
ImmutableSet.<String>builder()
|
||||
.add("DomainBase")
|
||||
.addAll(toKindStrings(PHASE_ONE_ORDERED))
|
||||
.addAll(toKindStrings(PHASE_TWO_ORDERED))
|
||||
.build()));
|
||||
|
||||
// Set up the pipeline to write entity kinds from PHASE_ONE_ORDERED to SQL. Return a object
|
||||
// that signals the completion of the phase.
|
||||
PCollection<Void> blocker =
|
||||
scheduleOnePhaseWrites(datastoreSnapshot, PHASE_ONE_ORDERED, Optional.empty(), null);
|
||||
blocker =
|
||||
writeToSql(
|
||||
"DomainBase without circular foreign keys",
|
||||
removeDomainBaseForeignKeys(datastoreSnapshot)
|
||||
.apply("Wait on phase one", Wait.on(blocker)));
|
||||
// Set up the pipeline to write entity kinds from PHASE_TWO_ORDERED to SQL. This phase won't
|
||||
// start until all cleansed DomainBases have been written (started by line above).
|
||||
scheduleOnePhaseWrites(
|
||||
datastoreSnapshot, PHASE_TWO_ORDERED, Optional.of(blocker), "DomainBaseNoFkeys");
|
||||
}
|
||||
|
||||
private PCollection<VersionedEntity> removeDomainBaseForeignKeys(
|
||||
PCollectionTuple datastoreSnapshot) {
|
||||
PCollection<VersionedEntity> domainBases =
|
||||
datastoreSnapshot.get(Transforms.createTagForKind("DomainBase"));
|
||||
return domainBases.apply(
|
||||
"Remove circular foreign keys from DomainBase",
|
||||
ParDo.of(new RemoveDomainBaseForeignKeys()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the pipeline to write entities in {@code entityClasses} to SQL. Entities are written
|
||||
* one kind at a time based on each kind's position in {@code entityClasses}. Concurrency exists
|
||||
* within each kind.
|
||||
*
|
||||
* @param datastoreSnapshot the Datastore snapshot of all data to be migrated to SQL
|
||||
* @param entityClasses the entity types in write order
|
||||
* @param blockingPCollection the pipeline stage that blocks this phase
|
||||
* @param blockingTag description of the stage (if exists) that blocks this phase. Needed for
|
||||
* generating unique transform ids
|
||||
* @return the output {@code PCollection} from the writing of the last entity kind. Other parts of
|
||||
* the pipeline can {@link Wait} on this object
|
||||
*/
|
||||
private PCollection<Void> scheduleOnePhaseWrites(
|
||||
PCollectionTuple datastoreSnapshot,
|
||||
Collection<Class<?>> entityClasses,
|
||||
Optional<PCollection<Void>> blockingPCollection,
|
||||
String blockingTag) {
|
||||
checkArgument(!entityClasses.isEmpty(), "Each phase must have at least one kind.");
|
||||
ImmutableList<TupleTag<VersionedEntity>> tags =
|
||||
toKindStrings(entityClasses).stream()
|
||||
.map(Transforms::createTagForKind)
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
|
||||
PCollection<Void> prev = blockingPCollection.orElse(null);
|
||||
String prevTag = blockingTag;
|
||||
for (TupleTag<VersionedEntity> tag : tags) {
|
||||
PCollection<VersionedEntity> curr = datastoreSnapshot.get(tag);
|
||||
if (prev != null) {
|
||||
curr = curr.apply("Wait on " + prevTag, Wait.on(prev));
|
||||
}
|
||||
prev = writeToSql(tag.getId(), curr);
|
||||
prevTag = tag.getId();
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
||||
private PCollection<Void> writeToSql(String transformId, PCollection<VersionedEntity> data) {
|
||||
String credentialFileUrl =
|
||||
options.getSqlCredentialUrlOverride() != null
|
||||
? options.getSqlCredentialUrlOverride()
|
||||
: BackupPaths.getCloudSQLCredentialFilePatterns(options.getEnvironment()).get(0);
|
||||
|
||||
return data.apply(
|
||||
"Write to sql: " + transformId,
|
||||
Transforms.writeToSql(
|
||||
transformId,
|
||||
options.getMaxConcurrentSqlWriters(),
|
||||
options.getSqlWriteBatchSize(),
|
||||
new JpaSupplierFactory(credentialFileUrl, options.getCloudKmsProjectId(), jpaGetter)));
|
||||
}
|
||||
|
||||
private static ImmutableList<String> toKindStrings(Collection<Class<?>> entityClasses) {
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
return entityClasses.stream().map(Key::getKind).collect(ImmutableList.toImmutableList());
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
InitSqlPipelineOptions options =
|
||||
PipelineOptionsFactory.fromArgs(args).withValidation().as(InitSqlPipelineOptions.class);
|
||||
new InitSqlPipeline(options).run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2020 The Nomulus 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.beam.initsql;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
|
||||
import org.apache.beam.sdk.options.Default;
|
||||
import org.apache.beam.sdk.options.Description;
|
||||
import org.apache.beam.sdk.options.Validation;
|
||||
|
||||
/** Pipeline options for {@link InitSqlPipeline} */
|
||||
public interface InitSqlPipelineOptions extends GcpOptions {
|
||||
|
||||
@Description(
|
||||
"Overrides the URL to the SQL credential file. " + "Required if environment is not provided.")
|
||||
@Nullable
|
||||
String getSqlCredentialUrlOverride();
|
||||
|
||||
void setSqlCredentialUrlOverride(String credentialUrlOverride);
|
||||
|
||||
@Description("The root directory of the export to load.")
|
||||
String getDatastoreExportDir();
|
||||
|
||||
void setDatastoreExportDir(String datastoreExportDir);
|
||||
|
||||
@Description("The directory that contains all CommitLog files.")
|
||||
String getCommitLogDir();
|
||||
|
||||
void setCommitLogDir(String commitLogDir);
|
||||
|
||||
@Description("The earliest CommitLogs to load, in ISO8601 format.")
|
||||
@Validation.Required
|
||||
String getCommitLogStartTimestamp();
|
||||
|
||||
void setCommitLogStartTimestamp(String commitLogStartTimestamp);
|
||||
|
||||
@Description("The latest CommitLogs to load, in ISO8601 format.")
|
||||
@Validation.Required
|
||||
String getCommitLogEndTimestamp();
|
||||
|
||||
void setCommitLogEndTimestamp(String commitLogEndTimestamp);
|
||||
|
||||
@Description(
|
||||
"The deployed environment, alpha, crash, sandbox, or production. "
|
||||
+ "Not required only if sqlCredentialUrlOverride is provided.")
|
||||
@Nullable
|
||||
String getEnvironment();
|
||||
|
||||
void setEnvironment(String environment);
|
||||
|
||||
@Description(
|
||||
"The GCP project that contains the keyring used for decrypting the " + "SQL credential file.")
|
||||
@Nullable
|
||||
String getCloudKmsProjectId();
|
||||
|
||||
void setCloudKmsProjectId(String cloudKmsProjectId);
|
||||
|
||||
@Description(
|
||||
"The maximum JDBC connection pool size on a VM. "
|
||||
+ "This value should be equal to or greater than the number of cores on the VM.")
|
||||
@Default.Integer(4)
|
||||
int getJdbcMaxPoolSize();
|
||||
|
||||
void setJdbcMaxPoolSize(int jdbcMaxPoolSize);
|
||||
|
||||
@Description(
|
||||
"A hint to the pipeline runner of the maximum number of concurrent SQL writers to create. "
|
||||
+ "Note that multiple writers may run on the same VM and share the connection pool.")
|
||||
@Default.Integer(4)
|
||||
int getMaxConcurrentSqlWriters();
|
||||
|
||||
void setMaxConcurrentSqlWriters(int maxConcurrentSqlWriters);
|
||||
|
||||
@Description("The number of entities to be written to the SQL database in one transaction.")
|
||||
@Default.Integer(20)
|
||||
int getSqlWriteBatchSize();
|
||||
|
||||
void setSqlWriteBatchSize(int sqlWriteBatchSize);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2020 The Nomulus 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.beam.initsql;
|
||||
|
||||
import google.registry.beam.initsql.BeamJpaModule.JpaTransactionManagerComponent;
|
||||
import google.registry.beam.initsql.Transforms.SerializableSupplier;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.beam.sdk.transforms.SerializableFunction;
|
||||
|
||||
public class JpaSupplierFactory implements SerializableSupplier<JpaTransactionManager> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String credentialFileUrl;
|
||||
@Nullable private final String cloudKmsProjectId;
|
||||
private final SerializableFunction<JpaTransactionManagerComponent, JpaTransactionManager>
|
||||
jpaGetter;
|
||||
|
||||
public JpaSupplierFactory(
|
||||
String credentialFileUrl,
|
||||
@Nullable String cloudKmsProjectId,
|
||||
SerializableFunction<JpaTransactionManagerComponent, JpaTransactionManager> jpaGetter) {
|
||||
this.credentialFileUrl = credentialFileUrl;
|
||||
this.cloudKmsProjectId = cloudKmsProjectId;
|
||||
this.jpaGetter = jpaGetter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaTransactionManager get() {
|
||||
return jpaGetter.apply(
|
||||
DaggerBeamJpaModule_JpaTransactionManagerComponent.builder()
|
||||
.beamJpaModule(new BeamJpaModule(credentialFileUrl, cloudKmsProjectId))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
## Summary
|
||||
|
||||
This package contains a BEAM pipeline that populates a Cloud SQL database from a Datastore backup.
|
||||
This package contains a BEAM pipeline that populates a Cloud SQL database from a
|
||||
Datastore backup. The pipeline uses an unsynchronized Datastore export and
|
||||
overlapping CommitLogs generated by the Nomulus server to recreate a consistent
|
||||
Datastore snapshot, and writes the data to a Cloud SQL instance.
|
||||
|
||||
## Pipeline Visualization
|
||||
|
||||
The golden flow graph of the InitSqlPipeline is saved both as a text-base
|
||||
[DOT file](../../../../../../test/resources/google/registry/beam/initsql/pipeline_golden.dot)
|
||||
and a
|
||||
[.png file](../../../../../../test/resources/google/registry/beam/initsql/pipeline_golden.png).
|
||||
A test compares the flow graph of the current pipeline with the golden graph,
|
||||
and will fail if changes are detected. When this happens, run the Gradle task
|
||||
':core:updateInitSqlPipelineGraph' to update the golden files and review the
|
||||
changes.
|
||||
|
||||
@@ -17,8 +17,10 @@ package google.registry.beam.initsql;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||
import static google.registry.beam.initsql.BackupPaths.getCommitLogTimestamp;
|
||||
import static google.registry.beam.initsql.BackupPaths.getExportFilePatterns;
|
||||
import static google.registry.persistence.JpaRetries.isFailedTxnRetriable;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.setJpaTm;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
@@ -29,14 +31,16 @@ import static org.apache.beam.sdk.values.TypeDescriptors.kvs;
|
||||
import static org.apache.beam.sdk.values.TypeDescriptors.strings;
|
||||
|
||||
import avro.shaded.com.google.common.collect.Iterators;
|
||||
import com.google.appengine.api.datastore.Entity;
|
||||
import com.google.appengine.api.datastore.EntityTranslator;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.backup.AppEngineEnvironment;
|
||||
import google.registry.backup.CommitLogImports;
|
||||
import google.registry.backup.VersionedEntity;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.ofy.ObjectifyService;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
@@ -49,7 +53,6 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.function.Supplier;
|
||||
import javax.persistence.OptimisticLockException;
|
||||
import org.apache.beam.sdk.coders.StringUtf8Coder;
|
||||
import org.apache.beam.sdk.io.Compression;
|
||||
import org.apache.beam.sdk.io.FileIO;
|
||||
@@ -70,7 +73,6 @@ import org.apache.beam.sdk.values.PBegin;
|
||||
import org.apache.beam.sdk.values.PCollection;
|
||||
import org.apache.beam.sdk.values.PCollectionList;
|
||||
import org.apache.beam.sdk.values.PCollectionTuple;
|
||||
import org.apache.beam.sdk.values.PDone;
|
||||
import org.apache.beam.sdk.values.TupleTag;
|
||||
import org.apache.beam.sdk.values.TupleTagList;
|
||||
import org.apache.beam.sdk.values.TypeDescriptor;
|
||||
@@ -225,7 +227,7 @@ public final class Transforms {
|
||||
return new PTransform<PCollection<String>, PCollection<Metadata>>() {
|
||||
@Override
|
||||
public PCollection<Metadata> expand(PCollection<String> input) {
|
||||
return input.apply(FileIO.matchAll().withEmptyMatchTreatment(EmptyMatchTreatment.DISALLOW));
|
||||
return input.apply(FileIO.matchAll().withEmptyMatchTreatment(EmptyMatchTreatment.ALLOW));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -263,6 +265,11 @@ public final class Transforms {
|
||||
|
||||
/**
|
||||
* Returns a {@link PTransform} that writes a {@link PCollection} of entities to a SQL database.
|
||||
* and outputs an empty {@code PCollection<Void>}. This allows other operations to {@link
|
||||
* org.apache.beam.sdk.transforms.Wait wait} for the completion of this transform.
|
||||
*
|
||||
* <p>Errors are handled according to the pipeline runner's default policy. As part of a one-time
|
||||
* job, we will not add features unless proven necessary.
|
||||
*
|
||||
* @param transformId a unique ID for an instance of the returned transform
|
||||
* @param maxWriters the max number of concurrent writes to SQL, which also determines the max
|
||||
@@ -270,22 +277,21 @@ public final class Transforms {
|
||||
* @param batchSize the number of entities to write in each operation
|
||||
* @param jpaSupplier supplier of a {@link JpaTransactionManager}
|
||||
*/
|
||||
public static PTransform<PCollection<VersionedEntity>, PDone> writeToSql(
|
||||
public static PTransform<PCollection<VersionedEntity>, PCollection<Void>> writeToSql(
|
||||
String transformId,
|
||||
int maxWriters,
|
||||
int batchSize,
|
||||
SerializableSupplier<JpaTransactionManager> jpaSupplier) {
|
||||
return new PTransform<PCollection<VersionedEntity>, PDone>() {
|
||||
return new PTransform<PCollection<VersionedEntity>, PCollection<Void>>() {
|
||||
@Override
|
||||
public PDone expand(PCollection<VersionedEntity> input) {
|
||||
input
|
||||
public PCollection<Void> expand(PCollection<VersionedEntity> input) {
|
||||
return input
|
||||
.apply(
|
||||
"Shard data for " + transformId,
|
||||
MapElements.into(kvs(integers(), TypeDescriptor.of(VersionedEntity.class)))
|
||||
.via(ve -> KV.of(ThreadLocalRandom.current().nextInt(maxWriters), ve)))
|
||||
.apply("Batch output by shard " + transformId, GroupIntoBatches.ofSize(batchSize))
|
||||
.apply("Write in batch for " + transformId, ParDo.of(new SqlBatchWriter(jpaSupplier)));
|
||||
return PDone.in(input.getPipeline());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -397,8 +403,10 @@ public final class Transforms {
|
||||
public void setup() {
|
||||
sleeper = new SystemSleeper();
|
||||
|
||||
ObjectifyService.initOfy();
|
||||
ofy = ObjectifyService.ofy();
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
ObjectifyService.initOfy();
|
||||
ofy = ObjectifyService.ofy();
|
||||
}
|
||||
|
||||
synchronized (SqlBatchWriter.class) {
|
||||
if (instanceCount == 0) {
|
||||
@@ -444,7 +452,10 @@ public final class Transforms {
|
||||
runnable.run();
|
||||
return;
|
||||
} catch (Throwable throwable) {
|
||||
throwIfNotCausedBy(throwable, OptimisticLockException.class);
|
||||
if (!isFailedTxnRetriable(throwable)) {
|
||||
throwIfUnchecked(throwable);
|
||||
throw new RuntimeException(throwable);
|
||||
}
|
||||
int sleepMillis = (1 << attempt) * initialDelayMillis;
|
||||
int jitter =
|
||||
ThreadLocalRandom.current().nextInt((int) (sleepMillis * jitterRatio))
|
||||
@@ -453,21 +464,28 @@ public final class Transforms {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rethrows {@code throwable} if it is not (and does not have a cause of) {@code causeType};
|
||||
* otherwise returns with no side effects.
|
||||
*/
|
||||
private void throwIfNotCausedBy(Throwable throwable, Class<? extends Throwable> causeType) {
|
||||
Throwable t = throwable;
|
||||
while (t != null) {
|
||||
if (causeType.isInstance(t)) {
|
||||
return;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
Throwables.throwIfUnchecked(t);
|
||||
throw new RuntimeException(t);
|
||||
/**
|
||||
* Removes BillingEvents, {@link google.registry.model.poll.PollMessage PollMessages} and {@link
|
||||
* google.registry.model.host.HostResource} from a {@link DomainBase}. These are circular foreign
|
||||
* key constraints that prevent migration of {@code DomainBase} to SQL databases.
|
||||
*
|
||||
* <p>See {@link InitSqlPipeline} for more information.
|
||||
*/
|
||||
static class RemoveDomainBaseForeignKeys extends DoFn<VersionedEntity, VersionedEntity> {
|
||||
|
||||
@ProcessElement
|
||||
public void processElement(
|
||||
@Element VersionedEntity domainBase, OutputReceiver<VersionedEntity> out) {
|
||||
checkArgument(
|
||||
domainBase.getEntity().isPresent(), "Unexpected delete entity %s", domainBase.key());
|
||||
Entity outputEntity =
|
||||
DomainBaseUtil.removeBillingAndPollAndHosts(domainBase.getEntity().get());
|
||||
out.output(
|
||||
VersionedEntity.from(
|
||||
domainBase.commitTimeMills(),
|
||||
EntityTranslator.convertToPb(outputEntity).toByteArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,15 @@ import static google.registry.beam.BeamUtils.getQueryFromFile;
|
||||
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.backup.AppEngineEnvironment;
|
||||
import google.registry.beam.initsql.Transforms.SerializableSupplier;
|
||||
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
|
||||
import google.registry.config.CredentialModule.LocalCredential;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.util.GoogleCredentialsBundle;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SqlTemplate;
|
||||
@@ -37,6 +43,7 @@ import org.apache.beam.sdk.options.Description;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.options.ValueProvider;
|
||||
import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider;
|
||||
import org.apache.beam.sdk.transforms.DoFn;
|
||||
import org.apache.beam.sdk.transforms.GroupByKey;
|
||||
import org.apache.beam.sdk.transforms.MapElements;
|
||||
import org.apache.beam.sdk.transforms.ParDo;
|
||||
@@ -46,6 +53,7 @@ import org.apache.beam.sdk.values.TypeDescriptor;
|
||||
import org.apache.beam.sdk.values.TypeDescriptors;
|
||||
import org.joda.time.LocalDate;
|
||||
import org.joda.time.YearMonth;
|
||||
import org.joda.time.format.ISODateTimeFormat;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -86,6 +94,7 @@ public class Spec11Pipeline implements Serializable {
|
||||
private final String reportingBucketUrl;
|
||||
private final GoogleCredentials googleCredentials;
|
||||
private final Retrier retrier;
|
||||
private final SerializableSupplier<JpaTransactionManager> jpaSupplierFactory;
|
||||
|
||||
@Inject
|
||||
public Spec11Pipeline(
|
||||
@@ -93,12 +102,14 @@ public class Spec11Pipeline implements Serializable {
|
||||
@Config("beamStagingUrl") String beamStagingUrl,
|
||||
@Config("spec11TemplateUrl") String spec11TemplateUrl,
|
||||
@Config("reportingBucketUrl") String reportingBucketUrl,
|
||||
SerializableSupplier<JpaTransactionManager> jpaSupplierFactory,
|
||||
@LocalCredential GoogleCredentialsBundle googleCredentialsBundle,
|
||||
Retrier retrier) {
|
||||
this.projectId = projectId;
|
||||
this.beamStagingUrl = beamStagingUrl;
|
||||
this.spec11TemplateUrl = spec11TemplateUrl;
|
||||
this.reportingBucketUrl = reportingBucketUrl;
|
||||
this.jpaSupplierFactory = jpaSupplierFactory;
|
||||
this.googleCredentials = googleCredentialsBundle.getGoogleCredentials();
|
||||
this.retrier = retrier;
|
||||
}
|
||||
@@ -177,12 +188,40 @@ public class Spec11Pipeline implements Serializable {
|
||||
EvaluateSafeBrowsingFn evaluateSafeBrowsingFn,
|
||||
ValueProvider<String> dateProvider) {
|
||||
|
||||
PCollection<KV<Subdomain, ThreatMatch>> subdomainsSql =
|
||||
domains.apply("Run through SafeBrowsing API", ParDo.of(evaluateSafeBrowsingFn));
|
||||
/* Store ThreatMatch objects in SQL. */
|
||||
subdomainsSql.apply(
|
||||
ParDo.of(
|
||||
new DoFn<KV<Subdomain, ThreatMatch>, Void>() {
|
||||
@ProcessElement
|
||||
public void processElement(ProcessContext context) {
|
||||
// create the Spec11ThreatMatch from Subdomain and ThreatMatch
|
||||
try (AppEngineEnvironment env = new AppEngineEnvironment()) {
|
||||
Subdomain subdomain = context.element().getKey();
|
||||
Spec11ThreatMatch threatMatch =
|
||||
new Spec11ThreatMatch.Builder()
|
||||
.setThreatTypes(
|
||||
ImmutableSet.of(
|
||||
ThreatType.valueOf(context.element().getValue().threatType())))
|
||||
.setCheckDate(
|
||||
LocalDate.parse(dateProvider.get(), ISODateTimeFormat.date()))
|
||||
.setDomainName(subdomain.domainName())
|
||||
.setDomainRepoId(subdomain.domainRepoId())
|
||||
.setRegistrarId(subdomain.registrarId())
|
||||
.build();
|
||||
JpaTransactionManager jpaTransactionManager = jpaSupplierFactory.get();
|
||||
jpaTransactionManager.transact(() -> jpaTransactionManager.saveNew(threatMatch));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
/* Store ThreatMatch objects in JSON. */
|
||||
PCollection<KV<Subdomain, ThreatMatch>> subdomainsJson =
|
||||
domains.apply("Run through SafeBrowsingAPI", ParDo.of(evaluateSafeBrowsingFn));
|
||||
subdomainsJson
|
||||
.apply(
|
||||
"Map registrar client ID to email/ThreatMatch pair",
|
||||
"Map registrar ID to email/ThreatMatch pair",
|
||||
MapElements.into(
|
||||
TypeDescriptors.kvs(
|
||||
TypeDescriptors.strings(), TypeDescriptor.of(EmailAndThreatMatch.class)))
|
||||
|
||||
@@ -1527,6 +1527,21 @@ public final class RegistryConfig {
|
||||
return CONFIG_SETTINGS.get().hibernate.hikariIdleTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether to replicate cloud SQL transactions to datastore.
|
||||
*
|
||||
* <p>If true, all cloud SQL transactions will be persisted as TransactionEntity objects in the
|
||||
* Transaction table and replayed against datastore in a cron job.
|
||||
*/
|
||||
public static boolean getCloudSqlReplicateTransactions() {
|
||||
return CONFIG_SETTINGS.get().cloudSql.replicateTransactions;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void overrideCloudSqlReplicateTransactions(boolean replicateTransactions) {
|
||||
CONFIG_SETTINGS.get().cloudSql.replicateTransactions = replicateTransactions;
|
||||
}
|
||||
|
||||
/** Returns the roid suffix to be used for the roids of all contacts and hosts. */
|
||||
public static String getContactAndHostRoidSuffix() {
|
||||
return CONFIG_SETTINGS.get().registryPolicy.contactAndHostRoidSuffix;
|
||||
|
||||
@@ -122,6 +122,7 @@ public class RegistryConfigSettings {
|
||||
public String jdbcUrl;
|
||||
public String username;
|
||||
public String instanceConnectionName;
|
||||
public boolean replicateTransactions;
|
||||
}
|
||||
|
||||
/** Configuration for Apache Beam (Cloud Dataflow). */
|
||||
|
||||
@@ -230,6 +230,9 @@ cloudSql:
|
||||
username: username
|
||||
# This name is used by Cloud SQL when connecting to the database.
|
||||
instanceConnectionName: project-id:region:instance-id
|
||||
# Set this to true to replicate cloud SQL transactions to datastore in the
|
||||
# background.
|
||||
replicateTransactions: false
|
||||
|
||||
cloudDns:
|
||||
# Set both properties to null in Production.
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.documentation;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.sun.javadoc.AnnotationDesc;
|
||||
import com.sun.javadoc.ClassDoc;
|
||||
import com.sun.javadoc.FieldDoc;
|
||||
import com.sun.javadoc.SeeTag;
|
||||
import com.sun.javadoc.Tag;
|
||||
import google.registry.model.eppoutput.Result.Code;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class to represent documentation information for a single EPP flow.
|
||||
*
|
||||
* <p>The static method getFlowDocs() on this class returns a list of FlowDocumentation
|
||||
* instances corresponding to the leaf flows in the flows package, constructing the instances
|
||||
* from class information returned from the javadoc system. Each instance has methods for
|
||||
* retrieving relevant information about the flow, such as a description, error conditions, etc.
|
||||
*/
|
||||
public class FlowDocumentation {
|
||||
|
||||
/** Constants for names of various relevant packages and classes. */
|
||||
static final String FLOW_PACKAGE_NAME = "google.registry.flows";
|
||||
static final String BASE_FLOW_CLASS_NAME = FLOW_PACKAGE_NAME + ".Flow";
|
||||
static final String EXCEPTION_CLASS_NAME = FLOW_PACKAGE_NAME + ".EppException";
|
||||
static final String CODE_ANNOTATION_NAME = EXCEPTION_CLASS_NAME + ".EppResultCode";
|
||||
|
||||
/** Name of the class for this flow. */
|
||||
private final String name;
|
||||
|
||||
/** Fully qualified name of the class for this flow. */
|
||||
private final String qualifiedName;
|
||||
|
||||
/** Name of the package in which this flow resides. */
|
||||
private final String packageName;
|
||||
|
||||
/** Class docs for the flow. */
|
||||
private final String classDocs;
|
||||
|
||||
/** Javadoc-tagged error conditions for this flow in list form. */
|
||||
private final List<ErrorCase> errors;
|
||||
|
||||
/** Javadoc-tagged error conditions for this flow, organized by underlying error code. */
|
||||
private final ListMultimap<Long, ErrorCase> errorsByCode;
|
||||
|
||||
/**
|
||||
* Creates a FlowDocumentation for this flow class using data from javadoc tags. Not public
|
||||
* because clients should get FlowDocumentation objects via the DocumentationGenerator class.
|
||||
*/
|
||||
protected FlowDocumentation(ClassDoc flowDoc) {
|
||||
name = flowDoc.name();
|
||||
qualifiedName = flowDoc.qualifiedName();
|
||||
packageName = flowDoc.containingPackage().name();
|
||||
classDocs = flowDoc.commentText();
|
||||
errors = new ArrayList<>();
|
||||
// Store error codes in sorted order, and leave reasons in insert order.
|
||||
errorsByCode =
|
||||
Multimaps.newListMultimap(new TreeMap<Long, Collection<ErrorCase>>(), ArrayList::new);
|
||||
parseTags(flowDoc);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getQualifiedName() {
|
||||
return qualifiedName;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public String getClassDocs() {
|
||||
return classDocs;
|
||||
}
|
||||
|
||||
public ImmutableList<ErrorCase> getErrors() {
|
||||
return ImmutableList.copyOf(errors);
|
||||
}
|
||||
|
||||
public ImmutableMultimap<Long, ErrorCase> getErrorsByCode() {
|
||||
return ImmutableMultimap.copyOf(errorsByCode);
|
||||
}
|
||||
|
||||
/** Iterates through javadoc tags on the underlying class and calls specific parsing methods. */
|
||||
private void parseTags(ClassDoc flowDoc) {
|
||||
for (Tag tag : flowDoc.tags()) {
|
||||
// Everything else is not a relevant tag.
|
||||
if ("@error".equals(tag.name())) {
|
||||
parseErrorTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Exception to throw when an @error tag cannot be parsed correctly. */
|
||||
private static class BadErrorTagFormatException extends IllegalStateException {
|
||||
/** Makes a message to use as a prefix for the reason passed up to the superclass. */
|
||||
private static String makeMessage(String reason, Tag tag) {
|
||||
return String.format("Bad @error tag format at %s - %s", tag.position(), reason);
|
||||
}
|
||||
|
||||
private BadErrorTagFormatException(String reason, Tag tag) {
|
||||
super(makeMessage(reason, tag));
|
||||
}
|
||||
|
||||
private BadErrorTagFormatException(String reason, Tag tag, Exception cause) {
|
||||
super(makeMessage(reason, tag), cause);
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses a javadoc tag corresponding to an error case and updates the error mapping. */
|
||||
private void parseErrorTag(Tag tag) {
|
||||
// Parse the @error tag text to find the @link inline tag.
|
||||
SeeTag linkedTag;
|
||||
try {
|
||||
linkedTag =
|
||||
Stream.of(tag.inlineTags())
|
||||
.filter(SeeTag.class::isInstance)
|
||||
.map(SeeTag.class::cast)
|
||||
.collect(onlyElement());
|
||||
} catch (NoSuchElementException | IllegalArgumentException e) {
|
||||
throw new BadErrorTagFormatException(
|
||||
String.format("expected one @link tag in tag text but found %s: %s",
|
||||
(e instanceof NoSuchElementException ? "none" : "multiple"),
|
||||
tag.text()),
|
||||
tag, e);
|
||||
}
|
||||
// Check to see if the @link tag references a valid class.
|
||||
ClassDoc exceptionRef = linkedTag.referencedClass();
|
||||
if (exceptionRef == null) {
|
||||
throw new BadErrorTagFormatException(
|
||||
"could not resolve class from @link tag text: " + linkedTag.text(),
|
||||
tag);
|
||||
}
|
||||
// Try to convert the referenced class into an ErrorCase; fail if it's not an EppException.
|
||||
ErrorCase error;
|
||||
try {
|
||||
error = new ErrorCase(exceptionRef);
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
throw new BadErrorTagFormatException(
|
||||
"class referenced in @link is not a valid EppException: " + exceptionRef.qualifiedName(),
|
||||
tag, e);
|
||||
}
|
||||
// Success; store this as a parsed error case.
|
||||
errors.add(error);
|
||||
errorsByCode.put(error.getCode(), error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an error case for a flow, with a reason for the error and the EPP error code.
|
||||
*
|
||||
* <p>This class is an immutable wrapper for the name of an EppException subclass that gets
|
||||
* thrown to indicate an error condition. It overrides equals() and hashCode() so that
|
||||
* instances of this class can be used in collections in the normal fashion.
|
||||
*/
|
||||
public static class ErrorCase {
|
||||
|
||||
/** The non-qualified name of the exception class. */
|
||||
private final String name;
|
||||
|
||||
/** The fully-qualified name of the exception class. */
|
||||
private final String className;
|
||||
|
||||
/** The reason this error was thrown, normally documented on the low-level exception class. */
|
||||
private final String reason;
|
||||
|
||||
/** The EPP error code value corresponding to this error condition. */
|
||||
private final long errorCode;
|
||||
|
||||
/** Constructs an ErrorCase from the corresponding class for a low-level flow exception. */
|
||||
protected ErrorCase(ClassDoc exceptionDoc) {
|
||||
name = exceptionDoc.name();
|
||||
className = exceptionDoc.qualifiedName();
|
||||
// The javadoc comment on the class explains the reason for the error condition.
|
||||
reason = exceptionDoc.commentText();
|
||||
ClassDoc highLevelExceptionDoc = getHighLevelExceptionFrom(exceptionDoc);
|
||||
errorCode = extractErrorCode(highLevelExceptionDoc);
|
||||
checkArgument(!exceptionDoc.isAbstract(),
|
||||
"Cannot use an abstract subclass of EppException as an error case");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
protected String getClassName() {
|
||||
return className;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public long getCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/** Returns the direct subclass of EppException that this class is a subclass of (or is). */
|
||||
private ClassDoc getHighLevelExceptionFrom(ClassDoc exceptionDoc) {
|
||||
// While we're not yet at the root, move up the class hierarchy looking for EppException.
|
||||
while (exceptionDoc.superclass() != null) {
|
||||
if (exceptionDoc.superclass().qualifiedTypeName().equals(EXCEPTION_CLASS_NAME)) {
|
||||
return exceptionDoc;
|
||||
}
|
||||
exceptionDoc = exceptionDoc.superclass();
|
||||
}
|
||||
// Failure; we reached the root without finding a subclass of EppException.
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Class referenced is not a subclass of %s", EXCEPTION_CLASS_NAME));
|
||||
}
|
||||
|
||||
/** Returns the corresponding EPP error code for an annotated subclass of EppException. */
|
||||
private long extractErrorCode(ClassDoc exceptionDoc) {
|
||||
try {
|
||||
// We're looking for a specific annotation by name that should appear only once.
|
||||
AnnotationDesc errorCodeAnnotation =
|
||||
Arrays.stream(exceptionDoc.annotations())
|
||||
.filter(
|
||||
anno -> anno.annotationType().qualifiedTypeName().equals(CODE_ANNOTATION_NAME))
|
||||
.findFirst()
|
||||
.get();
|
||||
// The annotation should have one element whose value converts to an EppResult.Code.
|
||||
AnnotationDesc.ElementValuePair pair = errorCodeAnnotation.elementValues()[0];
|
||||
String enumConstant = ((FieldDoc) pair.value().value()).name();
|
||||
return Code.valueOf(enumConstant).code;
|
||||
} catch (IllegalStateException e) {
|
||||
throw new IllegalStateException(
|
||||
"No error code annotation found on exception " + exceptionDoc.name(), e);
|
||||
} catch (ArrayIndexOutOfBoundsException | ClassCastException | IllegalArgumentException e) {
|
||||
throw new IllegalStateException("Bad annotation on exception " + exceptionDoc.name(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object object) {
|
||||
// The className field canonically identifies the EppException wrapped by this class, and
|
||||
// all other instance state is derived from that exception, so we only check className.
|
||||
return object instanceof ErrorCase && this.className.equals(((ErrorCase) object).className);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// See note for equals() - only className is needed for comparisons.
|
||||
return className.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright 2017 The Nomulus 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.documentation;
|
||||
|
||||
import static google.registry.util.BuildPathUtils.getProjectRoot;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.sun.javadoc.RootDoc;
|
||||
import com.sun.tools.javac.file.JavacFileManager;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import com.sun.tools.javac.util.ListBuffer;
|
||||
import com.sun.tools.javadoc.JavadocTool;
|
||||
import com.sun.tools.javadoc.Messager;
|
||||
import com.sun.tools.javadoc.ModifierFilter;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.tools.StandardLocation;
|
||||
|
||||
/**
|
||||
* Wrapper class to simplify calls to the javadoc system and hide internal details. An instance
|
||||
* represents a set of parameters for calling out to javadoc; these parameters can be set via
|
||||
* the appropriate methods, and determine what files and packages javadoc will process. The
|
||||
* actual running of javadoc occurs when calling getRootDoc() to retrieve a javadoc RootDoc.
|
||||
*/
|
||||
public final class JavadocWrapper {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** Shows any member visible at at least the default (package) level. */
|
||||
private static final long VISIBILITY_MASK =
|
||||
Modifier.PUBLIC | Modifier.PROTECTED | ModifierFilter.PACKAGE;
|
||||
|
||||
/** Root directory for source files. If null, will use the current directory. */
|
||||
private static final String SOURCE_PATH = getProjectRoot().resolve("core/src/main/java")
|
||||
.toString();
|
||||
/** Specific source files to generate documentation for. */
|
||||
private static final ImmutableSet<String> SOURCE_FILE_NAMES = ImmutableSet.of();
|
||||
|
||||
/** Specific packages to generate documentation for. */
|
||||
private static final ImmutableSet<String> SOURCE_PACKAGE_NAMES =
|
||||
ImmutableSet.of(FlowDocumentation.FLOW_PACKAGE_NAME);
|
||||
|
||||
/** Whether or not the Javadoc tool should eschew excessive log output. */
|
||||
private static final boolean QUIET = true;
|
||||
|
||||
/**
|
||||
* Obtains a Javadoc {@link RootDoc} object containing raw Javadoc documentation.
|
||||
* Wraps a call to the static method createRootDoc() and passes in instance-specific settings.
|
||||
*/
|
||||
public static RootDoc getRootDoc() throws IOException {
|
||||
logger.atInfo().log("Starting Javadoc tool");
|
||||
File sourceFilePath = new File(SOURCE_PATH);
|
||||
logger.atInfo().log("Using source directory: %s", sourceFilePath.getAbsolutePath());
|
||||
try {
|
||||
return createRootDoc(
|
||||
SOURCE_PATH,
|
||||
SOURCE_PACKAGE_NAMES,
|
||||
SOURCE_FILE_NAMES,
|
||||
VISIBILITY_MASK,
|
||||
QUIET);
|
||||
} finally {
|
||||
logger.atInfo().log("Javadoc tool finished");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a Javadoc root document object for the specified source path and package/Java names.
|
||||
* If the source path is null, then the working directory is assumed as the source path.
|
||||
*
|
||||
* <p>If a list of package names is provided, then Javadoc will run on these packages and all
|
||||
* their subpackages, based out of the specified source path.
|
||||
*
|
||||
* <p>If a list of file names is provided, then Javadoc will also run on these Java source files.
|
||||
* The specified source path is not considered in this case.
|
||||
*
|
||||
* @see <a href="http://relation.to/12969.lace">Testing Java doclets</a>
|
||||
* @see <a href="http://www.docjar.com/docs/api/com/sun/tools/javadoc/JavadocTool.html">JavadocTool</a>
|
||||
*/
|
||||
private static RootDoc createRootDoc(
|
||||
@Nullable String sourcePath,
|
||||
Collection<String> packageNames,
|
||||
Collection<String> fileNames,
|
||||
long visibilityMask,
|
||||
boolean quiet) throws IOException {
|
||||
// Create a context to hold settings for Javadoc.
|
||||
Context context = new Context();
|
||||
|
||||
// Redirect Javadoc stdout/stderr to null writers, since otherwise the Java compiler
|
||||
// issues lots of errors for classes that are imported and visible to blaze but not
|
||||
// visible locally to the compiler.
|
||||
// TODO(b/19124943): Find a way to ignore those errors so we can show real ones?
|
||||
Messager.preRegister(
|
||||
context,
|
||||
JavadocWrapper.class.getName(),
|
||||
new PrintWriter(CharStreams.nullWriter()), // For errors.
|
||||
new PrintWriter(CharStreams.nullWriter()), // For warnings.
|
||||
new PrintWriter(CharStreams.nullWriter())); // For notices.
|
||||
|
||||
// Set source path option for Javadoc.
|
||||
try (JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8)) {
|
||||
List<File> sourcePathFiles = new ArrayList<>();
|
||||
if (sourcePath != null) {
|
||||
for (String sourcePathEntry : Splitter.on(':').split(sourcePath)) {
|
||||
sourcePathFiles.add(new File(sourcePathEntry));
|
||||
}
|
||||
}
|
||||
fileManager.setLocation(StandardLocation.SOURCE_PATH, sourcePathFiles);
|
||||
|
||||
// Create an instance of Javadoc.
|
||||
JavadocTool javadocTool = JavadocTool.make0(context);
|
||||
|
||||
// Convert the package and file lists to a format Javadoc can understand.
|
||||
ListBuffer<String> subPackages = new ListBuffer<>();
|
||||
subPackages.addAll(packageNames);
|
||||
ListBuffer<String> javaNames = new ListBuffer<>();
|
||||
javaNames.addAll(fileNames);
|
||||
|
||||
// Invoke Javadoc and ask it for a RootDoc containing the specified packages.
|
||||
return javadocTool.getRootDocImpl(
|
||||
Locale.US.toString(), // Javadoc comment locale
|
||||
UTF_8.name(), // Source character encoding
|
||||
new ModifierFilter(visibilityMask), // Element visibility filter
|
||||
javaNames.toList(), // Included Java file names
|
||||
com.sun.tools.javac.util.List.nil(), // Doclet options
|
||||
com.sun.tools.javac.util.List.nil(), // Source files
|
||||
false, // Don't use BreakIterator
|
||||
subPackages.toList(), // Included sub-package names
|
||||
com.sun.tools.javac.util.List.nil(), // Excluded package names
|
||||
false, // Read source files, not classes
|
||||
false, // Don't run legacy doclet
|
||||
quiet); // If asked, run Javadoc quietly
|
||||
}
|
||||
}
|
||||
|
||||
private JavadocWrapper() {}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2017 The Nomulus 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.
|
||||
|
||||
|
||||
# Generate javadoc for the project
|
||||
|
||||
if (( $# != 3 )); then
|
||||
echo "Usage: $0 JAVADOC ZIP OUT" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVADOC_BINARY="$1"
|
||||
ZIP_BINARY="$2"
|
||||
TARGETFILE="$3"
|
||||
TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/generate_javadoc.XXXXXXXX")"
|
||||
PWDDIR="$(pwd)"
|
||||
|
||||
"${JAVADOC_BINARY}" -d "${TMPDIR}" \
|
||||
$(find java -name \*.java) \
|
||||
-tag error:t:'EPP Errors' \
|
||||
-subpackages google.registry \
|
||||
-exclude google.registry.dns:google.registry.proxy:google.registry.monitoring.blackbox
|
||||
cd "${TMPDIR}"
|
||||
"${PWDDIR}/${ZIP_BINARY}" -rXoq "${PWDDIR}/${TARGETFILE}" .
|
||||
cd -
|
||||
rm -rf "${TMPDIR}"
|
||||
@@ -42,6 +42,7 @@ import google.registry.model.EppResource.ForeignKeyedEppResource;
|
||||
import google.registry.model.EppResource.ResourceWithTransferData;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.DomainContent;
|
||||
import google.registry.model.domain.Period;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.eppcommon.AuthInfo;
|
||||
@@ -255,7 +256,7 @@ public final class ResourceFlowUtils {
|
||||
* @param domain is the domain already projected at approvalTime
|
||||
*/
|
||||
public static DateTime computeExDateForApprovalTime(
|
||||
DomainBase domain, DateTime approvalTime, Period period) {
|
||||
DomainContent domain, DateTime approvalTime, Period period) {
|
||||
boolean inAutoRenew = domain.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW);
|
||||
// inAutoRenew is set to false if the period is zero because a zero-period transfer should not
|
||||
// subsume an autorenew.
|
||||
|
||||
@@ -347,8 +347,8 @@ public class DomainCreateFlow implements TransactionalFlow {
|
||||
.setRepoId(repoId)
|
||||
.setIdnTableName(validateDomainNameWithIdnTables(domainName))
|
||||
.setRegistrationExpirationTime(registrationExpirationTime)
|
||||
.setAutorenewBillingEvent(Key.create(autorenewBillingEvent))
|
||||
.setAutorenewPollMessage(Key.create(autorenewPollMessage))
|
||||
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
|
||||
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
|
||||
.setLaunchNotice(hasClaimsNotice ? launchCreate.get().getNotice() : null)
|
||||
.setSmdId(signedMarkId)
|
||||
.setDsData(secDnsCreate.isPresent() ? secDnsCreate.get().getDsData() : null)
|
||||
@@ -360,7 +360,8 @@ public class DomainCreateFlow implements TransactionalFlow {
|
||||
command.getNameservers().stream().collect(toImmutableSet()))
|
||||
.setStatusValues(statuses.build())
|
||||
.setContacts(command.getContacts())
|
||||
.addGracePeriod(GracePeriod.forBillingEvent(GracePeriodStatus.ADD, createBillingEvent))
|
||||
.addGracePeriod(
|
||||
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, repoId, createBillingEvent))
|
||||
.build();
|
||||
entitiesToSave.add(
|
||||
newDomain,
|
||||
|
||||
@@ -31,7 +31,6 @@ import static google.registry.model.ResourceTransferUtils.handlePendingTransferO
|
||||
import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexDeletionTime;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.ADD_FIELDS;
|
||||
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.RENEW_FIELDS;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
@@ -187,14 +186,18 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
DateTime redemptionTime = now.plus(redemptionGracePeriodLength);
|
||||
asyncTaskEnqueuer.enqueueAsyncResave(
|
||||
existingDomain, now, ImmutableSortedSet.of(redemptionTime, deletionTime));
|
||||
builder.setDeletionTime(deletionTime)
|
||||
builder
|
||||
.setDeletionTime(deletionTime)
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
|
||||
// Clear out all old grace periods and add REDEMPTION, which does not include a key to a
|
||||
// billing event because there isn't one for a domain delete.
|
||||
.setGracePeriods(ImmutableSet.of(GracePeriod.createWithoutBillingEvent(
|
||||
GracePeriodStatus.REDEMPTION,
|
||||
redemptionTime,
|
||||
clientId)));
|
||||
.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.createWithoutBillingEvent(
|
||||
GracePeriodStatus.REDEMPTION,
|
||||
existingDomain.getRepoId(),
|
||||
redemptionTime,
|
||||
clientId)));
|
||||
// Note: The expiration time is unchanged, so if it's before the new deletion time, there will
|
||||
// be a "phantom autorenew" where the expiration time advances. No poll message will be
|
||||
// produced (since we are ending the autorenew recurrences at "now" below) and the billing
|
||||
@@ -209,7 +212,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
PollMessage.OneTime deletePollMessage =
|
||||
createDeletePollMessage(existingDomain, historyEntry, deletionTime);
|
||||
entitiesToSave.add(deletePollMessage);
|
||||
builder.setDeletePollMessage(Key.create(deletePollMessage));
|
||||
builder.setDeletePollMessage(deletePollMessage.createVKey());
|
||||
}
|
||||
|
||||
// Cancel any grace periods that were still active, and set the expiration time accordingly.
|
||||
@@ -222,8 +225,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
if (gracePeriod.getOneTimeBillingEvent() != null) {
|
||||
// Take the amount of amount of registration time being refunded off the expiration time.
|
||||
// This can be either add grace periods or renew grace periods.
|
||||
BillingEvent.OneTime oneTime =
|
||||
ofy().load().key(gracePeriod.getOneTimeBillingEvent()).now();
|
||||
BillingEvent.OneTime oneTime = tm().load(gracePeriod.getOneTimeBillingEvent());
|
||||
newExpirationTime = newExpirationTime.minusYears(oneTime.getPeriodYears());
|
||||
} else if (gracePeriod.getRecurringBillingEvent() != null) {
|
||||
// Take 1 year off the registration if in the autorenew grace period (no need to load the
|
||||
@@ -370,12 +372,12 @@ public final class DomainDeleteFlow implements TransactionalFlow {
|
||||
private Money getGracePeriodCost(GracePeriod gracePeriod, DateTime now) {
|
||||
if (gracePeriod.getType() == GracePeriodStatus.AUTO_RENEW) {
|
||||
DateTime autoRenewTime =
|
||||
ofy().load().key(checkNotNull(gracePeriod.getRecurringBillingEvent())).now()
|
||||
tm().load(checkNotNull(gracePeriod.getRecurringBillingEvent()))
|
||||
.getRecurrenceTimeOfYear()
|
||||
.getLastInstanceBeforeOrAt(now);
|
||||
.getLastInstanceBeforeOrAt(now);
|
||||
return getDomainRenewCost(targetId, autoRenewTime, 1);
|
||||
}
|
||||
return ofy().load().key(checkNotNull(gracePeriod.getOneTimeBillingEvent())).now().getCost();
|
||||
return tm().load(checkNotNull(gracePeriod.getOneTimeBillingEvent())).getCost();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -517,14 +517,14 @@ public class DomainFlowUtils {
|
||||
*/
|
||||
public static void updateAutorenewRecurrenceEndTime(DomainBase domain, DateTime newEndTime) {
|
||||
Optional<PollMessage.Autorenew> autorenewPollMessage =
|
||||
Optional.ofNullable(ofy().load().key(domain.getAutorenewPollMessage()).now());
|
||||
tm().maybeLoad(domain.getAutorenewPollMessage());
|
||||
|
||||
// Construct an updated autorenew poll message. If the autorenew poll message no longer exists,
|
||||
// create a new one at the same id. This can happen if a transfer was requested on a domain
|
||||
// where all autorenew poll messages had already been delivered (this would cause the poll
|
||||
// message to be deleted), and then subsequently the transfer was canceled, rejected, or deleted
|
||||
// (which would cause the poll message to be recreated here).
|
||||
Key<PollMessage.Autorenew> existingAutorenewKey = domain.getAutorenewPollMessage();
|
||||
Key<PollMessage.Autorenew> existingAutorenewKey = domain.getAutorenewPollMessage().getOfyKey();
|
||||
PollMessage.Autorenew updatedAutorenewPollMessage =
|
||||
autorenewPollMessage.isPresent()
|
||||
? autorenewPollMessage.get().asBuilder().setAutorenewEndTime(newEndTime).build()
|
||||
@@ -542,7 +542,7 @@ public class DomainFlowUtils {
|
||||
ofy().save().entity(updatedAutorenewPollMessage);
|
||||
}
|
||||
|
||||
Recurring recurring = ofy().load().key(domain.getAutorenewBillingEvent()).now();
|
||||
Recurring recurring = tm().load(domain.getAutorenewBillingEvent());
|
||||
ofy().save().entity(recurring.asBuilder().setRecurrenceEndTime(newEndTime).build());
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ public final class DomainPricingLogic {
|
||||
* <p>If {@code allocationToken} is present and the domain is non-premium, that discount will be
|
||||
* applied to the first year.
|
||||
*/
|
||||
public FeesAndCredits getCreatePrice(
|
||||
FeesAndCredits getCreatePrice(
|
||||
Registry registry,
|
||||
String domainName,
|
||||
DateTime dateTime,
|
||||
@@ -104,8 +104,8 @@ public final class DomainPricingLogic {
|
||||
|
||||
/** Returns a new renew price for the pricer. */
|
||||
@SuppressWarnings("unused")
|
||||
public FeesAndCredits getRenewPrice(
|
||||
Registry registry, String domainName, DateTime dateTime, int years) throws EppException {
|
||||
FeesAndCredits getRenewPrice(Registry registry, String domainName, DateTime dateTime, int years)
|
||||
throws EppException {
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
BigDecimal renewCost = domainPrices.getRenewCost().multipliedBy(years).getAmount();
|
||||
return customLogic.customizeRenewPrice(
|
||||
@@ -123,7 +123,7 @@ public final class DomainPricingLogic {
|
||||
}
|
||||
|
||||
/** Returns a new restore price for the pricer. */
|
||||
public FeesAndCredits getRestorePrice(
|
||||
FeesAndCredits getRestorePrice(
|
||||
Registry registry, String domainName, DateTime dateTime, boolean isExpired)
|
||||
throws EppException {
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
@@ -147,7 +147,7 @@ public final class DomainPricingLogic {
|
||||
}
|
||||
|
||||
/** Returns a new transfer price for the pricer. */
|
||||
public FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime dateTime)
|
||||
FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime dateTime)
|
||||
throws EppException {
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
return customLogic.customizeTransferPrice(
|
||||
@@ -168,7 +168,7 @@ public final class DomainPricingLogic {
|
||||
}
|
||||
|
||||
/** Returns a new update price for the pricer. */
|
||||
public FeesAndCredits getUpdatePrice(Registry registry, String domainName, DateTime dateTime)
|
||||
FeesAndCredits getUpdatePrice(Registry registry, String domainName, DateTime dateTime)
|
||||
throws EppException {
|
||||
CurrencyUnit currency = registry.getCurrency();
|
||||
BaseFee feeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.UPDATE, false);
|
||||
@@ -191,16 +191,20 @@ public final class DomainPricingLogic {
|
||||
throws EppException {
|
||||
if (allocationToken.isPresent()
|
||||
&& allocationToken.get().getDiscountFraction() != 0.0
|
||||
&& domainPrices.isPremium()) {
|
||||
&& domainPrices.isPremium()
|
||||
&& !allocationToken.get().shouldDiscountPremiums()) {
|
||||
throw new AllocationTokenInvalidForPremiumNameException();
|
||||
}
|
||||
Money oneYearCreateCost = domainPrices.getCreateCost();
|
||||
Money totalDomainCreateCost = oneYearCreateCost.multipliedBy(years);
|
||||
// If a discount is applicable, apply it only to the first year
|
||||
|
||||
// Apply the allocation token discount, if applicable.
|
||||
if (allocationToken.isPresent()) {
|
||||
int discountedYears = Math.min(years, allocationToken.get().getDiscountYears());
|
||||
Money discount =
|
||||
oneYearCreateCost.multipliedBy(
|
||||
allocationToken.get().getDiscountFraction(), RoundingMode.HALF_UP);
|
||||
discountedYears * allocationToken.get().getDiscountFraction(),
|
||||
RoundingMode.HALF_EVEN);
|
||||
totalDomainCreateCost = totalDomainCreateCost.minus(discount);
|
||||
}
|
||||
return totalDomainCreateCost;
|
||||
@@ -209,7 +213,7 @@ public final class DomainPricingLogic {
|
||||
/** An allocation token was provided that is invalid for premium domains. */
|
||||
public static class AllocationTokenInvalidForPremiumNameException
|
||||
extends CommandUseErrorException {
|
||||
public AllocationTokenInvalidForPremiumNameException() {
|
||||
AllocationTokenInvalidForPremiumNameException() {
|
||||
super("A nonzero discount code cannot be applied to premium domains");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import static google.registry.util.DateTimeUtils.leapSafeAddYears;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.ParameterValueRangeErrorException;
|
||||
import google.registry.flows.ExtensionManager;
|
||||
@@ -181,10 +180,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
|
||||
.setLastEppUpdateTime(now)
|
||||
.setLastEppUpdateClientId(clientId)
|
||||
.setRegistrationExpirationTime(newExpirationTime)
|
||||
.setAutorenewBillingEvent(Key.create(newAutorenewEvent))
|
||||
.setAutorenewPollMessage(Key.create(newAutorenewPollMessage))
|
||||
.setAutorenewBillingEvent(newAutorenewEvent.createVKey())
|
||||
.setAutorenewPollMessage(newAutorenewPollMessage.createVKey())
|
||||
.addGracePeriod(
|
||||
GracePeriod.forBillingEvent(GracePeriodStatus.RENEW, explicitRenewEvent))
|
||||
GracePeriod.forBillingEvent(
|
||||
GracePeriodStatus.RENEW, existingDomain.getRepoId(), explicitRenewEvent))
|
||||
.build();
|
||||
EntityChanges entityChanges =
|
||||
flowCustomLogic.beforeSave(
|
||||
|
||||
@@ -26,7 +26,6 @@ import static google.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
|
||||
import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexDeletionTime;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
@@ -174,8 +173,8 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
|
||||
existingDomain, newExpirationTime, autorenewEvent, autorenewPollMessage, now, clientId);
|
||||
updateForeignKeyIndexDeletionTime(newDomain);
|
||||
entitiesToSave.add(newDomain, historyEntry, autorenewEvent, autorenewPollMessage);
|
||||
ofy().save().entities(entitiesToSave.build());
|
||||
ofy().delete().key(existingDomain.getDeletePollMessage());
|
||||
tm().saveNewOrUpdateAll(entitiesToSave.build());
|
||||
tm().delete(existingDomain.getDeletePollMessage());
|
||||
dnsQueue.addDomainRefreshTask(existingDomain.getDomainName());
|
||||
return responseBuilder
|
||||
.setExtensions(createResponseExtensions(feesAndCredits, feeUpdate, isExpired))
|
||||
@@ -232,8 +231,8 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
|
||||
.setStatusValues(null)
|
||||
.setGracePeriods(null)
|
||||
.setDeletePollMessage(null)
|
||||
.setAutorenewBillingEvent(Key.create(autorenewEvent))
|
||||
.setAutorenewPollMessage(Key.create(autorenewPollMessage))
|
||||
.setAutorenewBillingEvent(autorenewEvent.createVKey())
|
||||
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
|
||||
.setLastEppUpdateTime(now)
|
||||
.setLastEppUpdateClientId(clientId)
|
||||
.build();
|
||||
|
||||
@@ -186,13 +186,16 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
|
||||
.setTransferredRegistrationExpirationTime(newExpirationTime)
|
||||
.build())
|
||||
.setRegistrationExpirationTime(newExpirationTime)
|
||||
.setAutorenewBillingEvent(Key.create(autorenewEvent))
|
||||
.setAutorenewPollMessage(Key.create(gainingClientAutorenewPollMessage))
|
||||
.setAutorenewBillingEvent(autorenewEvent.createVKey())
|
||||
.setAutorenewPollMessage(gainingClientAutorenewPollMessage.createVKey())
|
||||
// Remove all the old grace periods and add a new one for the transfer.
|
||||
.setGracePeriods(
|
||||
billingEvent.isPresent()
|
||||
? ImmutableSet.of(
|
||||
GracePeriod.forBillingEvent(GracePeriodStatus.TRANSFER, billingEvent.get()))
|
||||
GracePeriod.forBillingEvent(
|
||||
GracePeriodStatus.TRANSFER,
|
||||
existingDomain.getRepoId(),
|
||||
billingEvent.get()))
|
||||
: ImmutableSet.of())
|
||||
.setLastEppUpdateTime(now)
|
||||
.setLastEppUpdateClientId(clientId)
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package google.registry.model;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
@@ -30,6 +29,7 @@ import javax.xml.bind.annotation.XmlTransient;
|
||||
*/
|
||||
@MappedSuperclass
|
||||
public abstract class BackupGroupRoot extends ImmutableObject {
|
||||
|
||||
/**
|
||||
* An automatically managed timestamp of when this object was last written to Datastore.
|
||||
*
|
||||
@@ -40,7 +40,6 @@ public abstract class BackupGroupRoot extends ImmutableObject {
|
||||
// Prevents subclasses from unexpectedly accessing as property (e.g., HostResource), which would
|
||||
// require an unnecessary non-private setter method.
|
||||
@Access(AccessType.FIELD)
|
||||
@VisibleForTesting
|
||||
UpdateAutoTimestamp updateTimestamp = UpdateAutoTimestamp.create(null);
|
||||
|
||||
/** Get the {@link UpdateAutoTimestamp} for this entity. */
|
||||
|
||||
@@ -347,27 +347,11 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
|
||||
@Override
|
||||
public VKey<OneTime> createVKey() {
|
||||
return VKey.create(
|
||||
getClass(),
|
||||
parent.getParent().getName() + "/" + parent.getId() + "/" + getId(),
|
||||
Key.create(this));
|
||||
return VKey.create(getClass(), getId(), Key.create(this));
|
||||
}
|
||||
|
||||
public static VKey<OneTime> createVKey(Key<OneTime> key) {
|
||||
// TODO(b/159207551): As it stands, the SQL key generated here doesn't mesh with the primary
|
||||
// key type for the table, which is a single long integer.
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
Key parent = key.getParent();
|
||||
Key grandparent = (parent != null) ? parent.getParent() : null;
|
||||
String path =
|
||||
(grandparent != null ? grandparent.getName() : "")
|
||||
+ "/"
|
||||
+ (parent != null ? parent.getId() : "")
|
||||
+ "/"
|
||||
+ key.getId();
|
||||
return VKey.create(OneTime.class, path, key);
|
||||
return VKey.create(OneTime.class, key.getId(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -505,21 +489,11 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
|
||||
@Override
|
||||
public VKey<Recurring> createVKey() {
|
||||
return VKey.create(
|
||||
getClass(),
|
||||
parent.getParent().getName() + "/" + parent.getId() + "/" + getId(),
|
||||
Key.create(this));
|
||||
return VKey.create(getClass(), getId(), Key.create(this));
|
||||
}
|
||||
|
||||
public static VKey<Recurring> createVKey(Key<Recurring> key) {
|
||||
// TODO(b/159207551): As it stands, the SQL key generated here doesn't mesh with the primary
|
||||
// key type for the table, which is a single long integer.
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
String path =
|
||||
key.getParent().getParent().getName() + "/" + key.getParent().getId() + "/" + key.getId();
|
||||
return VKey.create(Recurring.class, path, key);
|
||||
return VKey.create(Recurring.class, key.getId(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -630,30 +604,20 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
.setParent(historyEntry);
|
||||
// Set the grace period's billing event using the appropriate Cancellation builder method.
|
||||
if (gracePeriod.getOneTimeBillingEvent() != null) {
|
||||
builder.setOneTimeEventKey(VKey.from(gracePeriod.getOneTimeBillingEvent()));
|
||||
builder.setOneTimeEventKey(gracePeriod.getOneTimeBillingEvent());
|
||||
} else if (gracePeriod.getRecurringBillingEvent() != null) {
|
||||
builder.setRecurringEventKey(VKey.from(gracePeriod.getRecurringBillingEvent()));
|
||||
builder.setRecurringEventKey(gracePeriod.getRecurringBillingEvent());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<Cancellation> createVKey() {
|
||||
return VKey.create(
|
||||
getClass(),
|
||||
parent.getParent().getName() + "/" + parent.getId() + "/" + getId(),
|
||||
Key.create(this));
|
||||
return VKey.create(getClass(), getId(), Key.create(this));
|
||||
}
|
||||
|
||||
public static VKey<Cancellation> createVKey(Key<Cancellation> key) {
|
||||
// TODO(b/159207551): As it stands, the SQL key generated here doesn't mesh with the primary
|
||||
// key type for the table, which is a single long integer.
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
String path =
|
||||
key.getParent().getParent().getName() + "/" + key.getParent().getId() + "/" + key.getId();
|
||||
return VKey.create(Cancellation.class, path, key);
|
||||
return VKey.create(Cancellation.class, key.getId(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -733,21 +697,11 @@ public abstract class BillingEvent extends ImmutableObject
|
||||
|
||||
@Override
|
||||
public VKey<Modification> createVKey() {
|
||||
return VKey.create(
|
||||
getClass(),
|
||||
parent.getParent().getName() + "/" + parent.getId() + "/" + getId(),
|
||||
Key.create(this));
|
||||
return VKey.create(getClass(), getId(), Key.create(this));
|
||||
}
|
||||
|
||||
public static VKey<Modification> createVKey(Key<Modification> key) {
|
||||
// TODO(b/159207551): As it stands, the SQL key generated here doesn't mesh with the primary
|
||||
// key type for the table, which is a single long integer.
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
String path =
|
||||
key.getParent().getParent().getName() + "/" + key.getParent().getId() + "/" + key.getId();
|
||||
return VKey.create(Modification.class, path, key);
|
||||
return VKey.create(Modification.class, key.getId(), key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,10 +44,11 @@ import org.joda.time.DateTime;
|
||||
/**
|
||||
* A persistable contact resource including mutable and non-mutable fields.
|
||||
*
|
||||
* <p>This class deliberately does not include an {@link javax.persistence.Id} so that any
|
||||
* foreign-keyed fields can refer to the proper parent entity's ID, whether we're storing this in
|
||||
* the DB itself or as part of another entity
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5733">RFC 5733</a>
|
||||
* <p>This class deliberately does not include an {@link javax.persistence.Id} so that any
|
||||
* foreign-keyed fields can refer to the proper parent entity's ID, whether we're storing this
|
||||
* in the DB itself or as part of another entity
|
||||
*/
|
||||
@MappedSuperclass
|
||||
@Embeddable
|
||||
|
||||
@@ -25,8 +25,8 @@ import javax.persistence.Entity;
|
||||
* A persisted history entry representing an EPP modification to a contact.
|
||||
*
|
||||
* <p>In addition to the general history fields (e.g. action time, registrar ID) we also persist a
|
||||
* copy of the host entity at this point in time. We persist a raw {@link ContactBase} so that the
|
||||
* foreign-keyed fields in that class can refer to this object.
|
||||
* copy of the contact entity at this point in time. We persist a raw {@link ContactBase} so that
|
||||
* the foreign-keyed fields in that class can refer to this object.
|
||||
*/
|
||||
@Entity
|
||||
@javax.persistence.Table(
|
||||
|
||||
@@ -14,79 +14,28 @@
|
||||
|
||||
package google.registry.model.domain;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
|
||||
import static google.registry.model.EppResourceUtils.setAutomaticTransferSuccessProperties;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static google.registry.util.CollectionUtils.union;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.earliestOf;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static google.registry.util.DomainNameUtils.getTldFromDomainName;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import com.googlecode.objectify.annotation.IgnoreSave;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import com.googlecode.objectify.annotation.OnLoad;
|
||||
import com.googlecode.objectify.condition.IfNull;
|
||||
import google.registry.flows.ResourceFlowUtils;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.EppResource.ForeignKeyedEppResource;
|
||||
import google.registry.model.EppResource.ResourceWithTransferData;
|
||||
import google.registry.model.annotations.ExternalMessagingName;
|
||||
import google.registry.model.annotations.ReportedOn;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.DesignatedContact.Type;
|
||||
import google.registry.model.domain.launch.LaunchNotice;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.persistence.WithStringVKey;
|
||||
import google.registry.schema.replay.DatastoreAndSqlEntity;
|
||||
import google.registry.util.CollectionUtils;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.ElementCollection;
|
||||
import javax.persistence.Embedded;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinTable;
|
||||
import javax.persistence.PostLoad;
|
||||
import javax.persistence.Transient;
|
||||
import javax.persistence.OneToMany;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
|
||||
/**
|
||||
* A persistable domain resource including mutable and non-mutable fields.
|
||||
@@ -112,194 +61,8 @@ import org.joda.time.Interval;
|
||||
@WithStringVKey
|
||||
@ExternalMessagingName("domain")
|
||||
@Access(AccessType.FIELD)
|
||||
public class DomainBase extends EppResource
|
||||
implements DatastoreAndSqlEntity,
|
||||
ForeignKeyedEppResource,
|
||||
ResourceWithTransferData<DomainTransferData> {
|
||||
|
||||
/** The max number of years that a domain can be registered for, as set by ICANN policy. */
|
||||
public static final int MAX_REGISTRATION_YEARS = 10;
|
||||
|
||||
/** Status values which prohibit DNS information from being published. */
|
||||
private static final ImmutableSet<StatusValue> DNS_PUBLISHING_PROHIBITED_STATUSES =
|
||||
ImmutableSet.of(
|
||||
StatusValue.CLIENT_HOLD,
|
||||
StatusValue.INACTIVE,
|
||||
StatusValue.PENDING_DELETE,
|
||||
StatusValue.SERVER_HOLD);
|
||||
|
||||
/**
|
||||
* Fully qualified domain name (puny-coded), which serves as the foreign key for this domain.
|
||||
*
|
||||
* <p>This is only unique in the sense that for any given lifetime specified as the time range
|
||||
* from (creationTime, deletionTime) there can only be one domain in Datastore with this name.
|
||||
* However, there can be many domains with the same name and non-overlapping lifetimes.
|
||||
*
|
||||
* @invariant fullyQualifiedDomainName == fullyQualifiedDomainName.toLowerCase(Locale.ENGLISH)
|
||||
*/
|
||||
// TODO(b/158858642): Rename this to domainName when we are off Datastore
|
||||
@Column(name = "domainName")
|
||||
@Index
|
||||
String fullyQualifiedDomainName;
|
||||
|
||||
/** The top level domain this is under, dernormalized from {@link #fullyQualifiedDomainName}. */
|
||||
@Index
|
||||
String tld;
|
||||
|
||||
/** References to hosts that are the nameservers for the domain. */
|
||||
@Index
|
||||
@ElementCollection
|
||||
@JoinTable(name = "DomainHost")
|
||||
Set<VKey<HostResource>> nsHosts;
|
||||
|
||||
/**
|
||||
* The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
|
||||
*
|
||||
* <p>These are stored in one field so that we can query across all contacts at once.
|
||||
*/
|
||||
@Transient Set<DesignatedContact> allContacts;
|
||||
|
||||
/**
|
||||
* Contacts as they are stored in cloud SQL.
|
||||
*
|
||||
* <p>This information is duplicated in allContacts, and must be kept in sync with it.
|
||||
*/
|
||||
@Ignore VKey<ContactResource> adminContact;
|
||||
|
||||
@Ignore VKey<ContactResource> billingContact;
|
||||
@Ignore VKey<ContactResource> techContact;
|
||||
@Ignore VKey<ContactResource> registrantContact;
|
||||
|
||||
/** Authorization info (aka transfer secret) of the domain. */
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
|
||||
@AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
|
||||
})
|
||||
DomainAuthInfo authInfo;
|
||||
|
||||
/**
|
||||
* Data used to construct DS records for this domain.
|
||||
*
|
||||
* <p>This is {@literal @}XmlTransient because it needs to be returned under the "extension" tag
|
||||
* of an info response rather than inside the "infData" tag.
|
||||
*/
|
||||
@Transient Set<DelegationSignerData> dsData;
|
||||
|
||||
/**
|
||||
* The claims notice supplied when this application or domain was created, if there was one. It's
|
||||
* {@literal @}XmlTransient because it's not returned in an info response.
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "noticeId.tcnId", column = @Column(name = "launch_notice_tcn_id")),
|
||||
@AttributeOverride(
|
||||
name = "noticeId.validatorId",
|
||||
column = @Column(name = "launch_notice_validator_id")),
|
||||
@AttributeOverride(
|
||||
name = "expirationTime",
|
||||
column = @Column(name = "launch_notice_expiration_time")),
|
||||
@AttributeOverride(
|
||||
name = "acceptedTime",
|
||||
column = @Column(name = "launch_notice_accepted_time")),
|
||||
})
|
||||
LaunchNotice launchNotice;
|
||||
|
||||
/**
|
||||
* Name of first IDN table associated with TLD that matched the characters in this domain label.
|
||||
*
|
||||
* @see google.registry.tldconfig.idn.IdnLabelValidator#findValidIdnTableForTld
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
String idnTableName;
|
||||
|
||||
/** Fully qualified host names of this domain's active subordinate hosts. */
|
||||
Set<String> subordinateHosts;
|
||||
|
||||
/** When this domain's registration will expire. */
|
||||
DateTime registrationExpirationTime;
|
||||
|
||||
/**
|
||||
* The poll message associated with this domain being deleted.
|
||||
*
|
||||
* <p>This field should be null if the domain is not in pending delete. If it is, the field should
|
||||
* refer to a {@link PollMessage} timed to when the domain is fully deleted. If the domain is
|
||||
* restored, the message should be deleted.
|
||||
*/
|
||||
@Transient Key<PollMessage.OneTime> deletePollMessage;
|
||||
|
||||
/**
|
||||
* The recurring billing event associated with this domain's autorenewals.
|
||||
*
|
||||
* <p>The recurrence should be open ended unless the domain is in pending delete or fully deleted,
|
||||
* in which case it should be closed at the time the delete was requested. Whenever the domain's
|
||||
* {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
|
||||
* should be created, and this field should be updated to point to the new one.
|
||||
*/
|
||||
@Transient Key<BillingEvent.Recurring> autorenewBillingEvent;
|
||||
|
||||
/**
|
||||
* The recurring poll message associated with this domain's autorenewals.
|
||||
*
|
||||
* <p>The recurrence should be open ended unless the domain is in pending delete or fully deleted,
|
||||
* in which case it should be closed at the time the delete was requested. Whenever the domain's
|
||||
* {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
|
||||
* should be created, and this field should be updated to point to the new one.
|
||||
*/
|
||||
@Transient Key<PollMessage.Autorenew> autorenewPollMessage;
|
||||
|
||||
/** The unexpired grace periods for this domain (some of which may not be active yet). */
|
||||
@Transient @ElementCollection Set<GracePeriod> gracePeriods;
|
||||
|
||||
/**
|
||||
* The id of the signed mark that was used to create this domain in sunrise.
|
||||
*
|
||||
* <p>Will only be populated for domains created in sunrise.
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
String smdId;
|
||||
|
||||
/** Data about any pending or past transfers on this domain. */
|
||||
DomainTransferData transferData;
|
||||
|
||||
/**
|
||||
* The time that this resource was last transferred.
|
||||
*
|
||||
* <p>Can be null if the resource has never been transferred.
|
||||
*/
|
||||
DateTime lastTransferTime;
|
||||
|
||||
@OnLoad
|
||||
void load() {
|
||||
// Reconstitute all of the contacts so that they have VKeys.
|
||||
allContacts =
|
||||
allContacts.stream().map(contact -> contact.reconstitute()).collect(toImmutableSet());
|
||||
setContactFields(allContacts, true);
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
void postLoad() {
|
||||
// Reconstitute the contact list.
|
||||
ImmutableSet.Builder<DesignatedContact> contactsBuilder =
|
||||
new ImmutableSet.Builder<DesignatedContact>();
|
||||
|
||||
if (registrantContact != null) {
|
||||
contactsBuilder.add(
|
||||
DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
|
||||
}
|
||||
if (billingContact != null) {
|
||||
contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
|
||||
}
|
||||
if (techContact != null) {
|
||||
contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
|
||||
}
|
||||
if (adminContact != null) {
|
||||
contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
|
||||
}
|
||||
|
||||
allContacts = contactsBuilder.build();
|
||||
}
|
||||
public class DomainBase extends DomainContent
|
||||
implements DatastoreAndSqlEntity, ForeignKeyedEppResource {
|
||||
|
||||
@Override
|
||||
@javax.persistence.Id
|
||||
@@ -308,334 +71,23 @@ public class DomainBase extends EppResource
|
||||
return super.getRepoId();
|
||||
}
|
||||
|
||||
public ImmutableSet<String> getSubordinateHosts() {
|
||||
return nullToEmptyImmutableCopy(subordinateHosts);
|
||||
@ElementCollection
|
||||
@JoinTable(name = "DomainHost")
|
||||
@Access(AccessType.PROPERTY)
|
||||
@Column(name = "host_repo_id")
|
||||
public Set<VKey<HostResource>> getNsHosts() {
|
||||
return super.nsHosts;
|
||||
}
|
||||
|
||||
public DateTime getRegistrationExpirationTime() {
|
||||
return registrationExpirationTime;
|
||||
}
|
||||
|
||||
public Key<PollMessage.OneTime> getDeletePollMessage() {
|
||||
return deletePollMessage;
|
||||
}
|
||||
|
||||
public Key<BillingEvent.Recurring> getAutorenewBillingEvent() {
|
||||
return autorenewBillingEvent;
|
||||
}
|
||||
|
||||
public Key<PollMessage.Autorenew> getAutorenewPollMessage() {
|
||||
return autorenewPollMessage;
|
||||
}
|
||||
|
||||
public ImmutableSet<GracePeriod> getGracePeriods() {
|
||||
return nullToEmptyImmutableCopy(gracePeriods);
|
||||
}
|
||||
|
||||
public String getSmdId() {
|
||||
return smdId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainTransferData getTransferData() {
|
||||
return Optional.ofNullable(transferData).orElse(DomainTransferData.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime getLastTransferTime() {
|
||||
return lastTransferTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getForeignKey() {
|
||||
return fullyQualifiedDomainName;
|
||||
}
|
||||
|
||||
public String getDomainName() {
|
||||
return fullyQualifiedDomainName;
|
||||
}
|
||||
|
||||
public ImmutableSet<DelegationSignerData> getDsData() {
|
||||
return nullToEmptyImmutableCopy(dsData);
|
||||
}
|
||||
|
||||
public LaunchNotice getLaunchNotice() {
|
||||
return launchNotice;
|
||||
}
|
||||
|
||||
public String getIdnTableName() {
|
||||
return idnTableName;
|
||||
}
|
||||
|
||||
public ImmutableSet<VKey<HostResource>> getNameservers() {
|
||||
return nullToEmptyImmutableCopy(nsHosts);
|
||||
}
|
||||
|
||||
public final String getCurrentSponsorClientId() {
|
||||
return getPersistedCurrentSponsorClientId();
|
||||
}
|
||||
|
||||
/** Returns true if DNS information should be published for the given domain. */
|
||||
public boolean shouldPublishToDns() {
|
||||
return intersection(getStatusValues(), DNS_PUBLISHING_PROHIBITED_STATUSES).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Registry Grace Period Statuses for this domain.
|
||||
*
|
||||
* <p>This collects all statuses from the domain's {@link GracePeriod} entries and also adds the
|
||||
* PENDING_DELETE status if needed.
|
||||
*/
|
||||
public ImmutableSet<GracePeriodStatus> getGracePeriodStatuses() {
|
||||
Set<GracePeriodStatus> gracePeriodStatuses = new HashSet<>();
|
||||
for (GracePeriod gracePeriod : getGracePeriods()) {
|
||||
gracePeriodStatuses.add(gracePeriod.getType());
|
||||
}
|
||||
if (getStatusValues().contains(StatusValue.PENDING_DELETE)
|
||||
&& !gracePeriodStatuses.contains(GracePeriodStatus.REDEMPTION)) {
|
||||
gracePeriodStatuses.add(GracePeriodStatus.PENDING_DELETE);
|
||||
}
|
||||
return ImmutableSet.copyOf(gracePeriodStatuses);
|
||||
}
|
||||
|
||||
/** Returns the subset of grace periods having the specified type. */
|
||||
public ImmutableSet<GracePeriod> getGracePeriodsOfType(GracePeriodStatus gracePeriodType) {
|
||||
ImmutableSet.Builder<GracePeriod> builder = new ImmutableSet.Builder<>();
|
||||
for (GracePeriod gracePeriod : getGracePeriods()) {
|
||||
if (gracePeriod.getType() == gracePeriodType) {
|
||||
builder.add(gracePeriod);
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* The logic in this method, which handles implicit server approval of transfers, very closely
|
||||
* parallels the logic in {@code DomainTransferApproveFlow} which handles explicit client
|
||||
* approvals.
|
||||
*/
|
||||
@Override
|
||||
public DomainBase cloneProjectedAtTime(final DateTime now) {
|
||||
|
||||
DomainTransferData transferData = getTransferData();
|
||||
DateTime transferExpirationTime = transferData.getPendingTransferExpirationTime();
|
||||
|
||||
// If there's a pending transfer that has expired, handle it.
|
||||
if (TransferStatus.PENDING.equals(transferData.getTransferStatus())
|
||||
&& isBeforeOrAt(transferExpirationTime, now)) {
|
||||
// Project until just before the transfer time. This will handle the case of an autorenew
|
||||
// before the transfer was even requested or during the request period.
|
||||
// If the transfer time is precisely the moment that the domain expires, there will not be an
|
||||
// autorenew billing event (since we end the recurrence at transfer time and recurrences are
|
||||
// exclusive of their ending), and we can just proceed with the transfer.
|
||||
DomainBase domainAtTransferTime =
|
||||
cloneProjectedAtTime(transferExpirationTime.minusMillis(1));
|
||||
|
||||
DateTime expirationDate = transferData.getTransferredRegistrationExpirationTime();
|
||||
if (expirationDate == null) {
|
||||
// Extend the registration by the correct number of years from the expiration time
|
||||
// that was current on the domain right before the transfer, capped at 10 years from
|
||||
// the moment of the transfer.
|
||||
expirationDate =
|
||||
ResourceFlowUtils.computeExDateForApprovalTime(
|
||||
domainAtTransferTime, transferExpirationTime, transferData.getTransferPeriod());
|
||||
}
|
||||
// If we are within an autorenew grace period, the transfer will subsume the autorenew. There
|
||||
// will already be a cancellation written in advance by the transfer request flow, so we don't
|
||||
// need to worry about billing, but we do need to cancel out the expiration time increase.
|
||||
// The transfer period saved in the transfer data will be one year, unless the superuser
|
||||
// extension set the transfer period to zero.
|
||||
// Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends
|
||||
// all other graces).
|
||||
Builder builder =
|
||||
domainAtTransferTime
|
||||
.asBuilder()
|
||||
.setRegistrationExpirationTime(expirationDate)
|
||||
// Set the speculatively-written new autorenew events as the domain's autorenew
|
||||
// events.
|
||||
.setAutorenewBillingEvent(
|
||||
transferData.getServerApproveAutorenewEvent() == null
|
||||
? null
|
||||
: transferData.getServerApproveAutorenewEvent().getOfyKey())
|
||||
.setAutorenewPollMessage(
|
||||
transferData.getServerApproveAutorenewPollMessage() == null
|
||||
? null
|
||||
: transferData.getServerApproveAutorenewPollMessage().getOfyKey());
|
||||
if (transferData.getTransferPeriod().getValue() == 1) {
|
||||
// Set the grace period using a key to the prescheduled transfer billing event. Not using
|
||||
// GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch.
|
||||
builder.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.TRANSFER,
|
||||
transferExpirationTime.plus(
|
||||
Registry.get(getTld()).getTransferGracePeriodLength()),
|
||||
transferData.getGainingClientId(),
|
||||
transferData.getServerApproveBillingEvent() == null
|
||||
? null
|
||||
: transferData.getServerApproveBillingEvent().getOfyKey())));
|
||||
} else {
|
||||
// There won't be a billing event, so we don't need a grace period
|
||||
builder.setGracePeriods(ImmutableSet.of());
|
||||
}
|
||||
// Set all remaining transfer properties.
|
||||
setAutomaticTransferSuccessProperties(builder, transferData);
|
||||
builder
|
||||
.setLastEppUpdateTime(transferExpirationTime)
|
||||
.setLastEppUpdateClientId(transferData.getGainingClientId());
|
||||
// Finish projecting to now.
|
||||
return builder.build().cloneProjectedAtTime(now);
|
||||
}
|
||||
|
||||
Optional<DateTime> newLastEppUpdateTime = Optional.empty();
|
||||
|
||||
// There is no transfer. Do any necessary autorenews for active domains.
|
||||
|
||||
Builder builder = asBuilder();
|
||||
if (isBeforeOrAt(registrationExpirationTime, now) && END_OF_TIME.equals(getDeletionTime())) {
|
||||
// Autorenew by the number of years between the old expiration time and now.
|
||||
DateTime lastAutorenewTime = leapSafeAddYears(
|
||||
registrationExpirationTime,
|
||||
new Interval(registrationExpirationTime, now).toPeriod().getYears());
|
||||
DateTime newExpirationTime = lastAutorenewTime.plusYears(1);
|
||||
builder
|
||||
.setRegistrationExpirationTime(newExpirationTime)
|
||||
.addGracePeriod(
|
||||
GracePeriod.createForRecurring(
|
||||
GracePeriodStatus.AUTO_RENEW,
|
||||
lastAutorenewTime.plus(Registry.get(getTld()).getAutoRenewGracePeriodLength()),
|
||||
getCurrentSponsorClientId(),
|
||||
autorenewBillingEvent));
|
||||
newLastEppUpdateTime = Optional.of(lastAutorenewTime);
|
||||
}
|
||||
|
||||
// Remove any grace periods that have expired.
|
||||
DomainBase almostBuilt = builder.build();
|
||||
builder = almostBuilt.asBuilder();
|
||||
for (GracePeriod gracePeriod : almostBuilt.getGracePeriods()) {
|
||||
if (isBeforeOrAt(gracePeriod.getExpirationTime(), now)) {
|
||||
builder.removeGracePeriod(gracePeriod);
|
||||
if (!newLastEppUpdateTime.isPresent()
|
||||
|| isBeforeOrAt(newLastEppUpdateTime.get(), gracePeriod.getExpirationTime())) {
|
||||
newLastEppUpdateTime = Optional.of(gracePeriod.getExpirationTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It is possible that the lastEppUpdateClientId is different from current sponsor client
|
||||
// id, so we have to do the comparison instead of having one variable just storing the most
|
||||
// recent time.
|
||||
if (newLastEppUpdateTime.isPresent()) {
|
||||
if (getLastEppUpdateTime() == null
|
||||
|| newLastEppUpdateTime.get().isAfter(getLastEppUpdateTime())) {
|
||||
builder
|
||||
.setLastEppUpdateTime(newLastEppUpdateTime.get())
|
||||
.setLastEppUpdateClientId(getCurrentSponsorClientId());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle common properties like setting or unsetting linked status. This also handles the
|
||||
// general case of pending transfers for other resource types, but since we've always handled
|
||||
// a pending transfer by this point that's a no-op for domains.
|
||||
projectResourceOntoBuilderAtTime(almostBuilt, builder, now);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Return what the expiration time would be if the given number of years were added to it. */
|
||||
public static DateTime extendRegistrationWithCap(
|
||||
DateTime now,
|
||||
DateTime currentExpirationTime,
|
||||
@Nullable Integer extendedRegistrationYears) {
|
||||
// We must cap registration at the max years (aka 10), even if that truncates the last year.
|
||||
return earliestOf(
|
||||
leapSafeAddYears(
|
||||
currentExpirationTime,
|
||||
Optional.ofNullable(extendedRegistrationYears).orElse(0)),
|
||||
leapSafeAddYears(now, MAX_REGISTRATION_YEARS));
|
||||
}
|
||||
|
||||
/** Loads and returns the fully qualified host names of all linked nameservers. */
|
||||
public ImmutableSortedSet<String> loadNameserverHostNames() {
|
||||
return ofy()
|
||||
.load()
|
||||
.keys(getNameservers().stream().map(VKey::getOfyKey).collect(toImmutableSet()))
|
||||
.values()
|
||||
.stream()
|
||||
.map(HostResource::getHostName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
}
|
||||
|
||||
/** A key to the registrant who registered this domain. */
|
||||
public VKey<ContactResource> getRegistrant() {
|
||||
return registrantContact;
|
||||
}
|
||||
|
||||
public VKey<ContactResource> getAdminContact() {
|
||||
return adminContact;
|
||||
}
|
||||
|
||||
public VKey<ContactResource> getBillingContact() {
|
||||
return billingContact;
|
||||
}
|
||||
|
||||
public VKey<ContactResource> getTechContact() {
|
||||
return techContact;
|
||||
}
|
||||
|
||||
/** Associated contacts for the domain (other than registrant). */
|
||||
public ImmutableSet<DesignatedContact> getContacts() {
|
||||
return nullToEmpty(allContacts)
|
||||
.stream()
|
||||
.filter(IS_REGISTRANT.negate())
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
public DomainAuthInfo getAuthInfo() {
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/** Returns all referenced contacts from this domain or application. */
|
||||
public ImmutableSet<VKey<ContactResource>> getReferencedContacts() {
|
||||
return nullToEmptyImmutableCopy(allContacts)
|
||||
.stream()
|
||||
.map(DesignatedContact::getContactKey)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
public String getTld() {
|
||||
return tld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the individual contact fields from {@code contacts}.
|
||||
*
|
||||
* <p>The registrant field is only set if {@code includeRegistrant} is true, as this field needs
|
||||
* to be set in some circumstances but not in others.
|
||||
*/
|
||||
private void setContactFields(Set<DesignatedContact> contacts, boolean includeRegistrant) {
|
||||
|
||||
// Set the individual contact fields.
|
||||
for (DesignatedContact contact : contacts) {
|
||||
switch (contact.getType()) {
|
||||
case BILLING:
|
||||
billingContact = contact.getContactKey();
|
||||
break;
|
||||
case TECH:
|
||||
techContact = contact.getContactKey();
|
||||
break;
|
||||
case ADMIN:
|
||||
adminContact = contact.getContactKey();
|
||||
break;
|
||||
case REGISTRANT:
|
||||
if (includeRegistrant) {
|
||||
registrantContact = contact.getContactKey();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown contact resource type: " + contact.getType());
|
||||
}
|
||||
}
|
||||
@Access(AccessType.PROPERTY)
|
||||
@OneToMany(
|
||||
cascade = {CascadeType.ALL},
|
||||
fetch = FetchType.EAGER,
|
||||
orphanRemoval = true)
|
||||
@JoinColumn(name = "domainRepoId", referencedColumnName = "repoId")
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private Set<GracePeriod> getInternalGracePeriods() {
|
||||
return gracePeriods;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -643,9 +95,14 @@ public class DomainBase extends EppResource
|
||||
return VKey.create(DomainBase.class, getRepoId(), Key.create(this));
|
||||
}
|
||||
|
||||
/** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
|
||||
private static final Predicate<DesignatedContact> IS_REGISTRANT =
|
||||
(DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type);
|
||||
@Override
|
||||
public DomainBase cloneProjectedAtTime(final DateTime now) {
|
||||
return cloneDomainProjectedAtTime(this, now);
|
||||
}
|
||||
|
||||
public static VKey<DomainBase> createVKey(Key key) {
|
||||
return VKey.create(DomainBase.class, key.getName(), key);
|
||||
}
|
||||
|
||||
/** An override of {@link EppResource#asBuilder} with tighter typing. */
|
||||
@Override
|
||||
@@ -654,201 +111,12 @@ public class DomainBase extends EppResource
|
||||
}
|
||||
|
||||
/** A builder for constructing {@link DomainBase}, since it is immutable. */
|
||||
public static class Builder extends EppResource.Builder<DomainBase, Builder>
|
||||
implements BuilderWithTransferData<DomainTransferData, Builder> {
|
||||
public static class Builder extends DomainContent.Builder<DomainBase, Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
Builder(DomainBase instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainBase build() {
|
||||
DomainBase instance = getInstance();
|
||||
// If TransferData is totally empty, set it to null.
|
||||
if (DomainTransferData.EMPTY.equals(getInstance().transferData)) {
|
||||
setTransferData(null);
|
||||
}
|
||||
// A DomainBase has status INACTIVE if there are no nameservers.
|
||||
if (getInstance().getNameservers().isEmpty()) {
|
||||
addStatusValue(StatusValue.INACTIVE);
|
||||
} else { // There are nameservers, so make sure INACTIVE isn't there.
|
||||
removeStatusValue(StatusValue.INACTIVE);
|
||||
}
|
||||
|
||||
checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
|
||||
if (instance.getRegistrant() == null
|
||||
&& instance.allContacts.stream().anyMatch(IS_REGISTRANT)) {
|
||||
throw new IllegalArgumentException("registrant is null but is in allContacts");
|
||||
}
|
||||
checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
|
||||
instance.tld = getTldFromDomainName(instance.fullyQualifiedDomainName);
|
||||
return super.build();
|
||||
}
|
||||
|
||||
public Builder setDomainName(String domainName) {
|
||||
checkArgument(
|
||||
domainName.equals(canonicalizeDomainName(domainName)),
|
||||
"Domain name must be in puny-coded, lower-case form");
|
||||
getInstance().fullyQualifiedDomainName = domainName;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setDsData(ImmutableSet<DelegationSignerData> dsData) {
|
||||
getInstance().dsData = dsData;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setRegistrant(VKey<ContactResource> registrant) {
|
||||
// Replace the registrant contact inside allContacts.
|
||||
getInstance().allContacts = union(
|
||||
getInstance().getContacts(),
|
||||
DesignatedContact.create(Type.REGISTRANT, checkArgumentNotNull(registrant)));
|
||||
|
||||
// Set the registrant field specifically.
|
||||
getInstance().registrantContact = registrant;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setAuthInfo(DomainAuthInfo authInfo) {
|
||||
getInstance().authInfo = authInfo;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setNameservers(VKey<HostResource> nameserver) {
|
||||
getInstance().nsHosts = ImmutableSet.of(nameserver);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
|
||||
getInstance().nsHosts = forceEmptyToNull(nameservers);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder addNameserver(VKey<HostResource> nameserver) {
|
||||
return addNameservers(ImmutableSet.of(nameserver));
|
||||
}
|
||||
|
||||
public Builder addNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
|
||||
return setNameservers(
|
||||
ImmutableSet.copyOf(union(getInstance().getNameservers(), nameservers)));
|
||||
}
|
||||
|
||||
public Builder removeNameserver(VKey<HostResource> nameserver) {
|
||||
return removeNameservers(ImmutableSet.of(nameserver));
|
||||
}
|
||||
|
||||
public Builder removeNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
|
||||
return setNameservers(
|
||||
ImmutableSet.copyOf(difference(getInstance().getNameservers(), nameservers)));
|
||||
}
|
||||
|
||||
public Builder setContacts(DesignatedContact contact) {
|
||||
return setContacts(ImmutableSet.of(contact));
|
||||
}
|
||||
|
||||
public Builder setContacts(ImmutableSet<DesignatedContact> contacts) {
|
||||
checkArgument(contacts.stream().noneMatch(IS_REGISTRANT), "Registrant cannot be a contact");
|
||||
|
||||
// Replace the non-registrant contacts inside allContacts.
|
||||
getInstance().allContacts =
|
||||
Streams.concat(
|
||||
nullToEmpty(getInstance().allContacts).stream().filter(IS_REGISTRANT),
|
||||
contacts.stream())
|
||||
.collect(toImmutableSet());
|
||||
|
||||
// Set the individual fields.
|
||||
getInstance().setContactFields(contacts, false);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder addContacts(ImmutableSet<DesignatedContact> contacts) {
|
||||
return setContacts(ImmutableSet.copyOf(union(getInstance().getContacts(), contacts)));
|
||||
}
|
||||
|
||||
public Builder removeContacts(ImmutableSet<DesignatedContact> contacts) {
|
||||
return setContacts(ImmutableSet.copyOf(difference(getInstance().getContacts(), contacts)));
|
||||
}
|
||||
|
||||
public Builder setLaunchNotice(LaunchNotice launchNotice) {
|
||||
getInstance().launchNotice = launchNotice;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setIdnTableName(String idnTableName) {
|
||||
getInstance().idnTableName = idnTableName;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder setSubordinateHosts(ImmutableSet<String> subordinateHosts) {
|
||||
getInstance().subordinateHosts = subordinateHosts;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public Builder addSubordinateHost(String hostToAdd) {
|
||||
return setSubordinateHosts(ImmutableSet.copyOf(
|
||||
union(getInstance().getSubordinateHosts(), hostToAdd)));
|
||||
}
|
||||
|
||||
public Builder removeSubordinateHost(String hostToRemove) {
|
||||
return setSubordinateHosts(ImmutableSet.copyOf(
|
||||
CollectionUtils.difference(getInstance().getSubordinateHosts(), hostToRemove)));
|
||||
}
|
||||
|
||||
public Builder setRegistrationExpirationTime(DateTime registrationExpirationTime) {
|
||||
getInstance().registrationExpirationTime = registrationExpirationTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDeletePollMessage(Key<PollMessage.OneTime> deletePollMessage) {
|
||||
getInstance().deletePollMessage = deletePollMessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAutorenewBillingEvent(
|
||||
Key<BillingEvent.Recurring> autorenewBillingEvent) {
|
||||
getInstance().autorenewBillingEvent = autorenewBillingEvent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAutorenewPollMessage(
|
||||
Key<PollMessage.Autorenew> autorenewPollMessage) {
|
||||
getInstance().autorenewPollMessage = autorenewPollMessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSmdId(String smdId) {
|
||||
getInstance().smdId = smdId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setGracePeriods(ImmutableSet<GracePeriod> gracePeriods) {
|
||||
getInstance().gracePeriods = gracePeriods;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addGracePeriod(GracePeriod gracePeriod) {
|
||||
getInstance().gracePeriods = union(getInstance().getGracePeriods(), gracePeriod);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder removeGracePeriod(GracePeriod gracePeriod) {
|
||||
getInstance().gracePeriods = CollectionUtils
|
||||
.difference(getInstance().getGracePeriods(), gracePeriod);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setTransferData(DomainTransferData transferData) {
|
||||
getInstance().transferData = transferData;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setLastTransferTime(DateTime lastTransferTime) {
|
||||
getInstance().lastTransferTime = lastTransferTime;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
// Copyright 2017 The Nomulus 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.model.domain;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
|
||||
import static google.registry.model.EppResourceUtils.setAutomaticTransferSuccessProperties;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.util.CollectionUtils.forceEmptyToNull;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
|
||||
import static google.registry.util.CollectionUtils.union;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.earliestOf;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static google.registry.util.DomainNameUtils.getTldFromDomainName;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import com.googlecode.objectify.annotation.IgnoreSave;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import com.googlecode.objectify.annotation.OnLoad;
|
||||
import com.googlecode.objectify.condition.IfNull;
|
||||
import google.registry.flows.ResourceFlowUtils;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.EppResource.ResourceWithTransferData;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.domain.launch.LaunchNotice;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.model.domain.secdns.DelegationSignerData;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.poll.PollMessage;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.transfer.DomainTransferData;
|
||||
import google.registry.model.transfer.TransferStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CollectionUtils;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.AttributeOverride;
|
||||
import javax.persistence.AttributeOverrides;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Embeddable;
|
||||
import javax.persistence.Embedded;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import javax.persistence.PostLoad;
|
||||
import javax.persistence.Transient;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Interval;
|
||||
|
||||
/**
|
||||
* A persistable domain resource including mutable and non-mutable fields.
|
||||
*
|
||||
* <p>This class deliberately does not include an {@link javax.persistence.Id} so that any
|
||||
* foreign-keyed fields can refer to the proper parent entity's ID, whether we're storing this in
|
||||
* the DB itself or as part of another entity.
|
||||
*
|
||||
* <p>For historical reasons, the name of this class is "DomainContent". Ideally it would be
|
||||
* "DomainBase" for parallelism with the other {@link EppResource} entity classes, but because that
|
||||
* name is already taken by {@link DomainBase} (also for historical reasons), we can't use it. Once
|
||||
* we are no longer on Datastore, we can rename the classes.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5731">RFC 5731</a>
|
||||
*/
|
||||
@MappedSuperclass
|
||||
@Embeddable
|
||||
@Access(AccessType.FIELD)
|
||||
public class DomainContent extends EppResource
|
||||
implements ResourceWithTransferData<DomainTransferData> {
|
||||
|
||||
/** The max number of years that a domain can be registered for, as set by ICANN policy. */
|
||||
public static final int MAX_REGISTRATION_YEARS = 10;
|
||||
|
||||
/** Status values which prohibit DNS information from being published. */
|
||||
private static final ImmutableSet<StatusValue> DNS_PUBLISHING_PROHIBITED_STATUSES =
|
||||
ImmutableSet.of(
|
||||
StatusValue.CLIENT_HOLD,
|
||||
StatusValue.INACTIVE,
|
||||
StatusValue.PENDING_DELETE,
|
||||
StatusValue.SERVER_HOLD);
|
||||
|
||||
/**
|
||||
* Fully qualified domain name (puny-coded), which serves as the foreign key for this domain.
|
||||
*
|
||||
* <p>This is only unique in the sense that for any given lifetime specified as the time range
|
||||
* from (creationTime, deletionTime) there can only be one domain in Datastore with this name.
|
||||
* However, there can be many domains with the same name and non-overlapping lifetimes.
|
||||
*
|
||||
* @invariant fullyQualifiedDomainName == fullyQualifiedDomainName.toLowerCase(Locale.ENGLISH)
|
||||
*/
|
||||
// TODO(b/158858642): Rename this to domainName when we are off Datastore
|
||||
@Column(name = "domainName")
|
||||
@Index
|
||||
String fullyQualifiedDomainName;
|
||||
|
||||
/** The top level domain this is under, dernormalized from {@link #fullyQualifiedDomainName}. */
|
||||
@Index String tld;
|
||||
|
||||
/** References to hosts that are the nameservers for the domain. */
|
||||
@Index @Transient Set<VKey<HostResource>> nsHosts;
|
||||
|
||||
/**
|
||||
* The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
|
||||
*
|
||||
* <p>These are stored in one field so that we can query across all contacts at once.
|
||||
*/
|
||||
@Transient Set<DesignatedContact> allContacts;
|
||||
|
||||
/**
|
||||
* Contacts as they are stored in cloud SQL.
|
||||
*
|
||||
* <p>This information is duplicated in allContacts, and must be kept in sync with it.
|
||||
*/
|
||||
@Ignore VKey<ContactResource> adminContact;
|
||||
|
||||
@Ignore VKey<ContactResource> billingContact;
|
||||
@Ignore VKey<ContactResource> techContact;
|
||||
@Ignore VKey<ContactResource> registrantContact;
|
||||
|
||||
/** Authorization info (aka transfer secret) of the domain. */
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
|
||||
@AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
|
||||
})
|
||||
DomainAuthInfo authInfo;
|
||||
|
||||
/**
|
||||
* Data used to construct DS records for this domain.
|
||||
*
|
||||
* <p>This is {@literal @}XmlTransient because it needs to be returned under the "extension" tag
|
||||
* of an info response rather than inside the "infData" tag.
|
||||
*/
|
||||
@Transient Set<DelegationSignerData> dsData;
|
||||
|
||||
/**
|
||||
* The claims notice supplied when this application or domain was created, if there was one. It's
|
||||
* {@literal @}XmlTransient because it's not returned in an info response.
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "noticeId.tcnId", column = @Column(name = "launch_notice_tcn_id")),
|
||||
@AttributeOverride(
|
||||
name = "noticeId.validatorId",
|
||||
column = @Column(name = "launch_notice_validator_id")),
|
||||
@AttributeOverride(
|
||||
name = "expirationTime",
|
||||
column = @Column(name = "launch_notice_expiration_time")),
|
||||
@AttributeOverride(
|
||||
name = "acceptedTime",
|
||||
column = @Column(name = "launch_notice_accepted_time")),
|
||||
})
|
||||
LaunchNotice launchNotice;
|
||||
|
||||
/**
|
||||
* Name of first IDN table associated with TLD that matched the characters in this domain label.
|
||||
*
|
||||
* @see google.registry.tldconfig.idn.IdnLabelValidator#findValidIdnTableForTld
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
String idnTableName;
|
||||
|
||||
/** Fully qualified host names of this domain's active subordinate hosts. */
|
||||
Set<String> subordinateHosts;
|
||||
|
||||
/** When this domain's registration will expire. */
|
||||
DateTime registrationExpirationTime;
|
||||
|
||||
/**
|
||||
* The poll message associated with this domain being deleted.
|
||||
*
|
||||
* <p>This field should be null if the domain is not in pending delete. If it is, the field should
|
||||
* refer to a {@link PollMessage} timed to when the domain is fully deleted. If the domain is
|
||||
* restored, the message should be deleted.
|
||||
*/
|
||||
@Column(name = "deletion_poll_message_id")
|
||||
VKey<PollMessage.OneTime> deletePollMessage;
|
||||
|
||||
/**
|
||||
* The recurring billing event associated with this domain's autorenewals.
|
||||
*
|
||||
* <p>The recurrence should be open ended unless the domain is in pending delete or fully deleted,
|
||||
* in which case it should be closed at the time the delete was requested. Whenever the domain's
|
||||
* {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
|
||||
* should be created, and this field should be updated to point to the new one.
|
||||
*/
|
||||
@Column(name = "billing_recurrence_id")
|
||||
VKey<BillingEvent.Recurring> autorenewBillingEvent;
|
||||
|
||||
/**
|
||||
* The recurring poll message associated with this domain's autorenewals.
|
||||
*
|
||||
* <p>The recurrence should be open ended unless the domain is in pending delete or fully deleted,
|
||||
* in which case it should be closed at the time the delete was requested. Whenever the domain's
|
||||
* {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
|
||||
* should be created, and this field should be updated to point to the new one.
|
||||
*/
|
||||
@Column(name = "autorenew_poll_message_id")
|
||||
VKey<PollMessage.Autorenew> autorenewPollMessage;
|
||||
|
||||
/** The unexpired grace periods for this domain (some of which may not be active yet). */
|
||||
@Transient Set<GracePeriod> gracePeriods;
|
||||
|
||||
/**
|
||||
* The id of the signed mark that was used to create this domain in sunrise.
|
||||
*
|
||||
* <p>Will only be populated for domains created in sunrise.
|
||||
*/
|
||||
@IgnoreSave(IfNull.class)
|
||||
String smdId;
|
||||
|
||||
/** Data about any pending or past transfers on this domain. */
|
||||
DomainTransferData transferData;
|
||||
|
||||
/**
|
||||
* The time that this resource was last transferred.
|
||||
*
|
||||
* <p>Can be null if the resource has never been transferred.
|
||||
*/
|
||||
DateTime lastTransferTime;
|
||||
|
||||
@OnLoad
|
||||
void load() {
|
||||
// Reconstitute all of the contacts so that they have VKeys.
|
||||
allContacts =
|
||||
allContacts.stream().map(contact -> contact.reconstitute()).collect(toImmutableSet());
|
||||
setContactFields(allContacts, true);
|
||||
|
||||
// We have to return the cloned object here because the original object's
|
||||
// hashcode is not correct due to the change to its domainRepoId. The cloned
|
||||
// object will have a null hashcode so that it can get a recalculated hashcode
|
||||
// when its hashCode() is invoked.
|
||||
// TODO(b/162739503): Remove this after fully migrating to Cloud SQL.
|
||||
if (gracePeriods != null) {
|
||||
gracePeriods =
|
||||
gracePeriods.stream()
|
||||
.map(gracePeriod -> gracePeriod.cloneWithDomainRepoId(getRepoId()))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
void postLoad() {
|
||||
// Reconstitute the contact list.
|
||||
ImmutableSet.Builder<DesignatedContact> contactsBuilder =
|
||||
new ImmutableSet.Builder<DesignatedContact>();
|
||||
|
||||
if (registrantContact != null) {
|
||||
contactsBuilder.add(
|
||||
DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
|
||||
}
|
||||
if (billingContact != null) {
|
||||
contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
|
||||
}
|
||||
if (techContact != null) {
|
||||
contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
|
||||
}
|
||||
if (adminContact != null) {
|
||||
contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
|
||||
}
|
||||
|
||||
allContacts = contactsBuilder.build();
|
||||
}
|
||||
|
||||
public ImmutableSet<String> getSubordinateHosts() {
|
||||
return nullToEmptyImmutableCopy(subordinateHosts);
|
||||
}
|
||||
|
||||
public DateTime getRegistrationExpirationTime() {
|
||||
return registrationExpirationTime;
|
||||
}
|
||||
|
||||
public VKey<PollMessage.OneTime> getDeletePollMessage() {
|
||||
return deletePollMessage;
|
||||
}
|
||||
|
||||
public VKey<BillingEvent.Recurring> getAutorenewBillingEvent() {
|
||||
return autorenewBillingEvent;
|
||||
}
|
||||
|
||||
public VKey<PollMessage.Autorenew> getAutorenewPollMessage() {
|
||||
return autorenewPollMessage;
|
||||
}
|
||||
|
||||
public ImmutableSet<GracePeriod> getGracePeriods() {
|
||||
return nullToEmptyImmutableCopy(gracePeriods);
|
||||
}
|
||||
|
||||
public String getSmdId() {
|
||||
return smdId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainTransferData getTransferData() {
|
||||
return Optional.ofNullable(transferData).orElse(DomainTransferData.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime getLastTransferTime() {
|
||||
return lastTransferTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getForeignKey() {
|
||||
return fullyQualifiedDomainName;
|
||||
}
|
||||
|
||||
public String getDomainName() {
|
||||
return fullyQualifiedDomainName;
|
||||
}
|
||||
|
||||
public ImmutableSet<DelegationSignerData> getDsData() {
|
||||
return nullToEmptyImmutableCopy(dsData);
|
||||
}
|
||||
|
||||
public LaunchNotice getLaunchNotice() {
|
||||
return launchNotice;
|
||||
}
|
||||
|
||||
public String getIdnTableName() {
|
||||
return idnTableName;
|
||||
}
|
||||
|
||||
public ImmutableSet<VKey<HostResource>> getNameservers() {
|
||||
return nullToEmptyImmutableCopy(nsHosts);
|
||||
}
|
||||
|
||||
// Hibernate needs this in order to populate nsHosts but no one else should ever use it
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void setNsHosts(Set<VKey<HostResource>> nsHosts) {
|
||||
this.nsHosts = nsHosts;
|
||||
}
|
||||
|
||||
// Hibernate needs this in order to populate gracePeriods but no one else should ever use it
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void setInternalGracePeriods(Set<GracePeriod> gracePeriods) {
|
||||
this.gracePeriods = gracePeriods;
|
||||
}
|
||||
|
||||
public final String getCurrentSponsorClientId() {
|
||||
return getPersistedCurrentSponsorClientId();
|
||||
}
|
||||
|
||||
/** Returns true if DNS information should be published for the given domain. */
|
||||
public boolean shouldPublishToDns() {
|
||||
return intersection(getStatusValues(), DNS_PUBLISHING_PROHIBITED_STATUSES).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Registry Grace Period Statuses for this domain.
|
||||
*
|
||||
* <p>This collects all statuses from the domain's {@link GracePeriod} entries and also adds the
|
||||
* PENDING_DELETE status if needed.
|
||||
*/
|
||||
public ImmutableSet<GracePeriodStatus> getGracePeriodStatuses() {
|
||||
Set<GracePeriodStatus> gracePeriodStatuses = new HashSet<>();
|
||||
for (GracePeriod gracePeriod : getGracePeriods()) {
|
||||
gracePeriodStatuses.add(gracePeriod.getType());
|
||||
}
|
||||
if (getStatusValues().contains(StatusValue.PENDING_DELETE)
|
||||
&& !gracePeriodStatuses.contains(GracePeriodStatus.REDEMPTION)) {
|
||||
gracePeriodStatuses.add(GracePeriodStatus.PENDING_DELETE);
|
||||
}
|
||||
return ImmutableSet.copyOf(gracePeriodStatuses);
|
||||
}
|
||||
|
||||
/** Returns the subset of grace periods having the specified type. */
|
||||
public ImmutableSet<GracePeriod> getGracePeriodsOfType(GracePeriodStatus gracePeriodType) {
|
||||
ImmutableSet.Builder<GracePeriod> builder = new ImmutableSet.Builder<>();
|
||||
for (GracePeriod gracePeriod : getGracePeriods()) {
|
||||
if (gracePeriod.getType() == gracePeriodType) {
|
||||
builder.add(gracePeriod);
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainContent cloneProjectedAtTime(final DateTime now) {
|
||||
return cloneDomainProjectedAtTime(this, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* The logic in this method, which handles implicit server approval of transfers, very closely
|
||||
* parallels the logic in {@code DomainTransferApproveFlow} which handles explicit client
|
||||
* approvals.
|
||||
*/
|
||||
protected static <T extends DomainContent> T cloneDomainProjectedAtTime(T domain, DateTime now) {
|
||||
DomainTransferData transferData = domain.getTransferData();
|
||||
DateTime transferExpirationTime = transferData.getPendingTransferExpirationTime();
|
||||
|
||||
// If there's a pending transfer that has expired, handle it.
|
||||
if (TransferStatus.PENDING.equals(transferData.getTransferStatus())
|
||||
&& isBeforeOrAt(transferExpirationTime, now)) {
|
||||
// Project until just before the transfer time. This will handle the case of an autorenew
|
||||
// before the transfer was even requested or during the request period.
|
||||
// If the transfer time is precisely the moment that the domain expires, there will not be an
|
||||
// autorenew billing event (since we end the recurrence at transfer time and recurrences are
|
||||
// exclusive of their ending), and we can just proceed with the transfer.
|
||||
T domainAtTransferTime =
|
||||
cloneDomainProjectedAtTime(domain, transferExpirationTime.minusMillis(1));
|
||||
|
||||
DateTime expirationDate = transferData.getTransferredRegistrationExpirationTime();
|
||||
if (expirationDate == null) {
|
||||
// Extend the registration by the correct number of years from the expiration time
|
||||
// that was current on the domain right before the transfer, capped at 10 years from
|
||||
// the moment of the transfer.
|
||||
expirationDate =
|
||||
ResourceFlowUtils.computeExDateForApprovalTime(
|
||||
domainAtTransferTime, transferExpirationTime, transferData.getTransferPeriod());
|
||||
}
|
||||
// If we are within an autorenew grace period, the transfer will subsume the autorenew. There
|
||||
// will already be a cancellation written in advance by the transfer request flow, so we don't
|
||||
// need to worry about billing, but we do need to cancel out the expiration time increase.
|
||||
// The transfer period saved in the transfer data will be one year, unless the superuser
|
||||
// extension set the transfer period to zero.
|
||||
// Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends
|
||||
// all other graces).
|
||||
Builder builder =
|
||||
domainAtTransferTime
|
||||
.asBuilder()
|
||||
.setRegistrationExpirationTime(expirationDate)
|
||||
// Set the speculatively-written new autorenew events as the domain's autorenew
|
||||
// events.
|
||||
.setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent())
|
||||
.setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage());
|
||||
if (transferData.getTransferPeriod().getValue() == 1) {
|
||||
// Set the grace period using a key to the prescheduled transfer billing event. Not using
|
||||
// GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch.
|
||||
builder.setGracePeriods(
|
||||
ImmutableSet.of(
|
||||
GracePeriod.create(
|
||||
GracePeriodStatus.TRANSFER,
|
||||
domain.getRepoId(),
|
||||
transferExpirationTime.plus(
|
||||
Registry.get(domain.getTld()).getTransferGracePeriodLength()),
|
||||
transferData.getGainingClientId(),
|
||||
transferData.getServerApproveBillingEvent())));
|
||||
} else {
|
||||
// There won't be a billing event, so we don't need a grace period
|
||||
builder.setGracePeriods(ImmutableSet.of());
|
||||
}
|
||||
// Set all remaining transfer properties.
|
||||
setAutomaticTransferSuccessProperties(builder, transferData);
|
||||
builder
|
||||
.setLastEppUpdateTime(transferExpirationTime)
|
||||
.setLastEppUpdateClientId(transferData.getGainingClientId());
|
||||
// Finish projecting to now.
|
||||
return (T) builder.build().cloneProjectedAtTime(now);
|
||||
}
|
||||
|
||||
Optional<DateTime> newLastEppUpdateTime = Optional.empty();
|
||||
|
||||
// There is no transfer. Do any necessary autorenews for active domains.
|
||||
|
||||
Builder builder = domain.asBuilder();
|
||||
if (isBeforeOrAt(domain.getRegistrationExpirationTime(), now)
|
||||
&& END_OF_TIME.equals(domain.getDeletionTime())) {
|
||||
// Autorenew by the number of years between the old expiration time and now.
|
||||
DateTime lastAutorenewTime =
|
||||
leapSafeAddYears(
|
||||
domain.getRegistrationExpirationTime(),
|
||||
new Interval(domain.getRegistrationExpirationTime(), now).toPeriod().getYears());
|
||||
DateTime newExpirationTime = lastAutorenewTime.plusYears(1);
|
||||
builder
|
||||
.setRegistrationExpirationTime(newExpirationTime)
|
||||
.addGracePeriod(
|
||||
GracePeriod.createForRecurring(
|
||||
GracePeriodStatus.AUTO_RENEW,
|
||||
domain.getRepoId(),
|
||||
lastAutorenewTime.plus(
|
||||
Registry.get(domain.getTld()).getAutoRenewGracePeriodLength()),
|
||||
domain.getCurrentSponsorClientId(),
|
||||
domain.getAutorenewBillingEvent()));
|
||||
newLastEppUpdateTime = Optional.of(lastAutorenewTime);
|
||||
}
|
||||
|
||||
// Remove any grace periods that have expired.
|
||||
T almostBuilt = (T) builder.build();
|
||||
builder = almostBuilt.asBuilder();
|
||||
for (GracePeriod gracePeriod : almostBuilt.getGracePeriods()) {
|
||||
if (isBeforeOrAt(gracePeriod.getExpirationTime(), now)) {
|
||||
builder.removeGracePeriod(gracePeriod);
|
||||
if (!newLastEppUpdateTime.isPresent()
|
||||
|| isBeforeOrAt(newLastEppUpdateTime.get(), gracePeriod.getExpirationTime())) {
|
||||
newLastEppUpdateTime = Optional.of(gracePeriod.getExpirationTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It is possible that the lastEppUpdateClientId is different from current sponsor client
|
||||
// id, so we have to do the comparison instead of having one variable just storing the most
|
||||
// recent time.
|
||||
if (newLastEppUpdateTime.isPresent()) {
|
||||
if (domain.getLastEppUpdateTime() == null
|
||||
|| newLastEppUpdateTime.get().isAfter(domain.getLastEppUpdateTime())) {
|
||||
builder
|
||||
.setLastEppUpdateTime(newLastEppUpdateTime.get())
|
||||
.setLastEppUpdateClientId(domain.getCurrentSponsorClientId());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle common properties like setting or unsetting linked status. This also handles the
|
||||
// general case of pending transfers for other resource types, but since we've always handled
|
||||
// a pending transfer by this point that's a no-op for domains.
|
||||
projectResourceOntoBuilderAtTime(almostBuilt, builder, now);
|
||||
return (T) builder.build();
|
||||
}
|
||||
|
||||
/** Return what the expiration time would be if the given number of years were added to it. */
|
||||
public static DateTime extendRegistrationWithCap(
|
||||
DateTime now, DateTime currentExpirationTime, @Nullable Integer extendedRegistrationYears) {
|
||||
// We must cap registration at the max years (aka 10), even if that truncates the last year.
|
||||
return earliestOf(
|
||||
leapSafeAddYears(
|
||||
currentExpirationTime, Optional.ofNullable(extendedRegistrationYears).orElse(0)),
|
||||
leapSafeAddYears(now, MAX_REGISTRATION_YEARS));
|
||||
}
|
||||
|
||||
/** Loads and returns the fully qualified host names of all linked nameservers. */
|
||||
public ImmutableSortedSet<String> loadNameserverHostNames() {
|
||||
return ofy()
|
||||
.load()
|
||||
.keys(getNameservers().stream().map(VKey::getOfyKey).collect(toImmutableSet()))
|
||||
.values()
|
||||
.stream()
|
||||
.map(HostResource::getHostName)
|
||||
.collect(toImmutableSortedSet(Ordering.natural()));
|
||||
}
|
||||
|
||||
/** A key to the registrant who registered this domain. */
|
||||
public VKey<ContactResource> getRegistrant() {
|
||||
return registrantContact;
|
||||
}
|
||||
|
||||
public VKey<ContactResource> getAdminContact() {
|
||||
return adminContact;
|
||||
}
|
||||
|
||||
public VKey<ContactResource> getBillingContact() {
|
||||
return billingContact;
|
||||
}
|
||||
|
||||
public VKey<ContactResource> getTechContact() {
|
||||
return techContact;
|
||||
}
|
||||
|
||||
/** Associated contacts for the domain (other than registrant). */
|
||||
public ImmutableSet<DesignatedContact> getContacts() {
|
||||
return nullToEmpty(allContacts).stream()
|
||||
.filter(IS_REGISTRANT.negate())
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
public DomainAuthInfo getAuthInfo() {
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/** Returns all referenced contacts from this domain or application. */
|
||||
public ImmutableSet<VKey<ContactResource>> getReferencedContacts() {
|
||||
return nullToEmptyImmutableCopy(allContacts).stream()
|
||||
.map(DesignatedContact::getContactKey)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
public String getTld() {
|
||||
return tld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the individual contact fields from {@code contacts}.
|
||||
*
|
||||
* <p>The registrant field is only set if {@code includeRegistrant} is true, as this field needs
|
||||
* to be set in some circumstances but not in others.
|
||||
*/
|
||||
protected void setContactFields(Set<DesignatedContact> contacts, boolean includeRegistrant) {
|
||||
// Set the individual contact fields.
|
||||
for (DesignatedContact contact : contacts) {
|
||||
switch (contact.getType()) {
|
||||
case BILLING:
|
||||
billingContact = contact.getContactKey();
|
||||
break;
|
||||
case TECH:
|
||||
techContact = contact.getContactKey();
|
||||
break;
|
||||
case ADMIN:
|
||||
adminContact = contact.getContactKey();
|
||||
break;
|
||||
case REGISTRANT:
|
||||
if (includeRegistrant) {
|
||||
registrantContact = contact.getContactKey();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown contact resource type: " + contact.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VKey<DomainBase> createVKey() {
|
||||
return VKey.create(DomainBase.class, getRepoId(), Key.create(this));
|
||||
}
|
||||
|
||||
public static VKey<DomainBase> createVKey(Key key) {
|
||||
return VKey.create(DomainBase.class, key.getName(), key);
|
||||
}
|
||||
|
||||
/** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
|
||||
protected static final Predicate<DesignatedContact> IS_REGISTRANT =
|
||||
(DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type);
|
||||
|
||||
/** An override of {@link EppResource#asBuilder} with tighter typing. */
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder<>(clone(this));
|
||||
}
|
||||
|
||||
/** A builder for constructing {@link DomainBase}, since it is immutable. */
|
||||
public static class Builder<T extends DomainContent, B extends Builder<T, B>>
|
||||
extends EppResource.Builder<T, B> implements BuilderWithTransferData<DomainTransferData, B> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
Builder(T instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T build() {
|
||||
T instance = getInstance();
|
||||
// If TransferData is totally empty, set it to null.
|
||||
if (DomainTransferData.EMPTY.equals(getInstance().transferData)) {
|
||||
setTransferData(null);
|
||||
}
|
||||
// A DomainBase has status INACTIVE if there are no nameservers.
|
||||
if (getInstance().getNameservers().isEmpty()) {
|
||||
addStatusValue(StatusValue.INACTIVE);
|
||||
} else { // There are nameservers, so make sure INACTIVE isn't there.
|
||||
removeStatusValue(StatusValue.INACTIVE);
|
||||
}
|
||||
|
||||
checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
|
||||
if (instance.getRegistrant() == null
|
||||
&& instance.allContacts.stream().anyMatch(IS_REGISTRANT)) {
|
||||
throw new IllegalArgumentException("registrant is null but is in allContacts");
|
||||
}
|
||||
checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
|
||||
instance.tld = getTldFromDomainName(instance.fullyQualifiedDomainName);
|
||||
return super.build();
|
||||
}
|
||||
|
||||
public B setDomainName(String domainName) {
|
||||
checkArgument(
|
||||
domainName.equals(canonicalizeDomainName(domainName)),
|
||||
"Domain name must be in puny-coded, lower-case form");
|
||||
getInstance().fullyQualifiedDomainName = domainName;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setDsData(ImmutableSet<DelegationSignerData> dsData) {
|
||||
getInstance().dsData = dsData;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setRegistrant(VKey<ContactResource> registrant) {
|
||||
// Replace the registrant contact inside allContacts.
|
||||
getInstance().allContacts =
|
||||
union(
|
||||
getInstance().getContacts(),
|
||||
DesignatedContact.create(
|
||||
DesignatedContact.Type.REGISTRANT, checkArgumentNotNull(registrant)));
|
||||
|
||||
// Set the registrant field specifically.
|
||||
getInstance().registrantContact = registrant;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setAuthInfo(DomainAuthInfo authInfo) {
|
||||
getInstance().authInfo = authInfo;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setNameservers(VKey<HostResource> nameserver) {
|
||||
getInstance().nsHosts = ImmutableSet.of(nameserver);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
|
||||
getInstance().nsHosts = forceEmptyToNull(nameservers);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B addNameserver(VKey<HostResource> nameserver) {
|
||||
return addNameservers(ImmutableSet.of(nameserver));
|
||||
}
|
||||
|
||||
public B addNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
|
||||
return setNameservers(
|
||||
ImmutableSet.copyOf(Sets.union(getInstance().getNameservers(), nameservers)));
|
||||
}
|
||||
|
||||
public B removeNameserver(VKey<HostResource> nameserver) {
|
||||
return removeNameservers(ImmutableSet.of(nameserver));
|
||||
}
|
||||
|
||||
public B removeNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
|
||||
return setNameservers(
|
||||
ImmutableSet.copyOf(difference(getInstance().getNameservers(), nameservers)));
|
||||
}
|
||||
|
||||
public B setContacts(DesignatedContact contact) {
|
||||
return setContacts(ImmutableSet.of(contact));
|
||||
}
|
||||
|
||||
public B setContacts(ImmutableSet<DesignatedContact> contacts) {
|
||||
checkArgument(contacts.stream().noneMatch(IS_REGISTRANT), "Registrant cannot be a contact");
|
||||
|
||||
// Replace the non-registrant contacts inside allContacts.
|
||||
getInstance().allContacts =
|
||||
Streams.concat(
|
||||
nullToEmpty(getInstance().allContacts).stream().filter(IS_REGISTRANT),
|
||||
contacts.stream())
|
||||
.collect(toImmutableSet());
|
||||
|
||||
// Set the individual fields.
|
||||
getInstance().setContactFields(contacts, false);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B addContacts(ImmutableSet<DesignatedContact> contacts) {
|
||||
return setContacts(ImmutableSet.copyOf(Sets.union(getInstance().getContacts(), contacts)));
|
||||
}
|
||||
|
||||
public B removeContacts(ImmutableSet<DesignatedContact> contacts) {
|
||||
return setContacts(ImmutableSet.copyOf(difference(getInstance().getContacts(), contacts)));
|
||||
}
|
||||
|
||||
public B setLaunchNotice(LaunchNotice launchNotice) {
|
||||
getInstance().launchNotice = launchNotice;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setIdnTableName(String idnTableName) {
|
||||
getInstance().idnTableName = idnTableName;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setSubordinateHosts(ImmutableSet<String> subordinateHosts) {
|
||||
getInstance().subordinateHosts = subordinateHosts;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B addSubordinateHost(String hostToAdd) {
|
||||
return setSubordinateHosts(
|
||||
ImmutableSet.copyOf(union(getInstance().getSubordinateHosts(), hostToAdd)));
|
||||
}
|
||||
|
||||
public B removeSubordinateHost(String hostToRemove) {
|
||||
return setSubordinateHosts(
|
||||
ImmutableSet.copyOf(
|
||||
CollectionUtils.difference(getInstance().getSubordinateHosts(), hostToRemove)));
|
||||
}
|
||||
|
||||
public B setRegistrationExpirationTime(DateTime registrationExpirationTime) {
|
||||
getInstance().registrationExpirationTime = registrationExpirationTime;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setDeletePollMessage(VKey<PollMessage.OneTime> deletePollMessage) {
|
||||
getInstance().deletePollMessage = deletePollMessage;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setAutorenewBillingEvent(VKey<BillingEvent.Recurring> autorenewBillingEvent) {
|
||||
getInstance().autorenewBillingEvent = autorenewBillingEvent;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setAutorenewPollMessage(VKey<PollMessage.Autorenew> autorenewPollMessage) {
|
||||
getInstance().autorenewPollMessage = autorenewPollMessage;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setSmdId(String smdId) {
|
||||
getInstance().smdId = smdId;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B setGracePeriods(ImmutableSet<GracePeriod> gracePeriods) {
|
||||
getInstance().gracePeriods = gracePeriods;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B addGracePeriod(GracePeriod gracePeriod) {
|
||||
getInstance().gracePeriods = union(getInstance().getGracePeriods(), gracePeriod);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
public B removeGracePeriod(GracePeriod gracePeriod) {
|
||||
getInstance().gracePeriods =
|
||||
CollectionUtils.difference(getInstance().getGracePeriods(), gracePeriod);
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B setTransferData(DomainTransferData transferData) {
|
||||
getInstance().transferData = transferData;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B setLastTransferTime(DateTime lastTransferTime) {
|
||||
getInstance().lastTransferTime = lastTransferTime;
|
||||
return thisCastToDerived();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2020 The Nomulus 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.model.domain;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.contact.ContactResource;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.model.reporting.HistoryEntry;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Set;
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.ElementCollection;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.JoinTable;
|
||||
|
||||
/**
|
||||
* A persisted history entry representing an EPP modification to a domain.
|
||||
*
|
||||
* <p>In addition to the general history fields (e.g. action time, registrar ID) we also persist a
|
||||
* copy of the domain entity at this point in time. We persist a raw {@link DomainContent} so that
|
||||
* the foreign-keyed fields in that class can refer to this object.
|
||||
*/
|
||||
@Entity
|
||||
@javax.persistence.Table(
|
||||
indexes = {
|
||||
@javax.persistence.Index(columnList = "creationTime"),
|
||||
@javax.persistence.Index(columnList = "historyRegistrarId"),
|
||||
@javax.persistence.Index(columnList = "historyType"),
|
||||
@javax.persistence.Index(columnList = "historyModificationTime")
|
||||
})
|
||||
public class DomainHistory extends HistoryEntry {
|
||||
// Store DomainContent instead of DomainBase so we don't pick up its @Id
|
||||
DomainContent domainContent;
|
||||
|
||||
@Column(nullable = false)
|
||||
VKey<DomainBase> domainRepoId;
|
||||
|
||||
@ElementCollection
|
||||
@JoinTable(name = "DomainHistoryHost")
|
||||
@Access(AccessType.PROPERTY)
|
||||
@Column(name = "host_repo_id")
|
||||
public Set<VKey<HostResource>> getNsHosts() {
|
||||
return domainContent.nsHosts;
|
||||
}
|
||||
|
||||
/** The state of the {@link DomainContent} object at this point in time. */
|
||||
public DomainContent getDomainContent() {
|
||||
return domainContent;
|
||||
}
|
||||
|
||||
/** The key to the {@link ContactResource} this is based off of. */
|
||||
public VKey<DomainBase> getDomainRepoId() {
|
||||
return domainRepoId;
|
||||
}
|
||||
|
||||
// Hibernate needs this in order to populate nsHosts but no one else should ever use it
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private void setNsHosts(Set<VKey<HostResource>> nsHosts) {
|
||||
if (domainContent != null) {
|
||||
domainContent.nsHosts = nsHosts;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
public static class Builder extends HistoryEntry.Builder<DomainHistory, DomainHistory.Builder> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
public Builder(DomainHistory instance) {
|
||||
super(instance);
|
||||
}
|
||||
|
||||
public Builder setDomainContent(DomainContent domainContent) {
|
||||
getInstance().domainContent = domainContent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDomainRepoId(VKey<DomainBase> domainRepoId) {
|
||||
getInstance().domainRepoId = domainRepoId;
|
||||
domainRepoId.maybeGetOfyKey().ifPresent(parent -> getInstance().parent = parent);
|
||||
return this;
|
||||
}
|
||||
|
||||
// We can remove this once all HistoryEntries are converted to History objects
|
||||
@Override
|
||||
public Builder setParent(Key<? extends EppResource> parent) {
|
||||
super.setParent(parent);
|
||||
getInstance().domainRepoId = VKey.create(DomainBase.class, parent.getName(), parent);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,15 @@ package google.registry.model.domain;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.Recurring;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
@@ -36,80 +35,17 @@ import org.joda.time.DateTime;
|
||||
* the resource is loaded from Datastore.
|
||||
*/
|
||||
@Embed
|
||||
@javax.persistence.Entity
|
||||
public class GracePeriod extends ImmutableObject {
|
||||
|
||||
@javax.persistence.Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Ignore
|
||||
/** Unique id required for hibernate representation. */
|
||||
long id;
|
||||
|
||||
/** The type of grace period. */
|
||||
GracePeriodStatus type;
|
||||
|
||||
/** When the grace period ends. */
|
||||
DateTime expirationTime;
|
||||
|
||||
/** The registrar to bill. */
|
||||
@Column(name = "registrarId")
|
||||
String clientId;
|
||||
|
||||
/**
|
||||
* The one-time billing event corresponding to the action that triggered this grace period, or
|
||||
* null if not applicable. Not set for autorenew grace periods (which instead use the field
|
||||
* {@code billingEventRecurring}) or for redemption grace periods (since deletes have no cost).
|
||||
*/
|
||||
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
|
||||
Key<BillingEvent.OneTime> billingEventOneTime = null;
|
||||
|
||||
/**
|
||||
* The recurring billing event corresponding to the action that triggered this grace period, if
|
||||
* applicable - i.e. if the action was an autorenew - or null in all other cases.
|
||||
*/
|
||||
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
|
||||
Key<BillingEvent.Recurring> billingEventRecurring = null;
|
||||
|
||||
public GracePeriodStatus getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public DateTime getExpirationTime() {
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/** Returns true if this GracePeriod has an associated BillingEvent; i.e. if it's refundable. */
|
||||
public boolean hasBillingEvent() {
|
||||
return billingEventOneTime != null || billingEventRecurring != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the one time billing event. The value will only be non-null if the type of this grace
|
||||
* period is not AUTO_RENEW.
|
||||
*/
|
||||
|
||||
public Key<BillingEvent.OneTime> getOneTimeBillingEvent() {
|
||||
return billingEventOneTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recurring billing event. The value will only be non-null if the type of this grace
|
||||
* period is AUTO_RENEW.
|
||||
*/
|
||||
public Key<BillingEvent.Recurring> getRecurringBillingEvent() {
|
||||
return billingEventRecurring;
|
||||
}
|
||||
@Entity
|
||||
@Table(indexes = @Index(columnList = "domainRepoId"))
|
||||
public class GracePeriod extends GracePeriodBase {
|
||||
|
||||
private static GracePeriod createInternal(
|
||||
GracePeriodStatus type,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
@Nullable Key<BillingEvent.OneTime> billingEventOneTime,
|
||||
@Nullable Key<BillingEvent.Recurring> billingEventRecurring) {
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime,
|
||||
@Nullable VKey<BillingEvent.Recurring> billingEventRecurring) {
|
||||
checkArgument((billingEventOneTime == null) || (billingEventRecurring == null),
|
||||
"A grace period can have at most one billing event");
|
||||
checkArgument(
|
||||
@@ -117,6 +53,7 @@ public class GracePeriod extends ImmutableObject {
|
||||
"Recurring billing events must be present on (and only on) autorenew grace periods");
|
||||
GracePeriod instance = new GracePeriod();
|
||||
instance.type = checkArgumentNotNull(type);
|
||||
instance.domainRepoId = checkArgumentNotNull(domainRepoId);
|
||||
instance.expirationTime = checkArgumentNotNull(expirationTime);
|
||||
instance.clientId = checkArgumentNotNull(clientId);
|
||||
instance.billingEventOneTime = billingEventOneTime;
|
||||
@@ -127,38 +64,57 @@ public class GracePeriod extends ImmutableObject {
|
||||
/**
|
||||
* Creates a GracePeriod for an (optional) OneTime billing event.
|
||||
*
|
||||
* <p>Normal callers should always use {@link #forBillingEvent} instead, assuming they do not
|
||||
* need to avoid loading the BillingEvent from Datastore. This method should typically be
|
||||
* called only from test code to explicitly construct GracePeriods.
|
||||
* <p>Normal callers should always use {@link #forBillingEvent} instead, assuming they do not need
|
||||
* to avoid loading the BillingEvent from Datastore. This method should typically be called only
|
||||
* from test code to explicitly construct GracePeriods.
|
||||
*/
|
||||
public static GracePeriod create(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
@Nullable Key<BillingEvent.OneTime> billingEventOneTime) {
|
||||
return createInternal(type, expirationTime, clientId, billingEventOneTime, null);
|
||||
@Nullable VKey<BillingEvent.OneTime> billingEventOneTime) {
|
||||
return createInternal(type, domainRepoId, expirationTime, clientId, billingEventOneTime, null);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod for a Recurring billing event. */
|
||||
public static GracePeriod createForRecurring(
|
||||
GracePeriodStatus type,
|
||||
String domainRepoId,
|
||||
DateTime expirationTime,
|
||||
String clientId,
|
||||
Key<BillingEvent.Recurring> billingEventRecurring) {
|
||||
VKey<Recurring> billingEventRecurring) {
|
||||
checkArgumentNotNull(billingEventRecurring, "billingEventRecurring cannot be null");
|
||||
return createInternal(type, expirationTime, clientId, null, billingEventRecurring);
|
||||
return createInternal(
|
||||
type, domainRepoId, expirationTime, clientId, null, billingEventRecurring);
|
||||
}
|
||||
|
||||
/** Creates a GracePeriod with no billing event. */
|
||||
public static GracePeriod createWithoutBillingEvent(
|
||||
GracePeriodStatus type, DateTime expirationTime, String clientId) {
|
||||
return createInternal(type, expirationTime, clientId, null, null);
|
||||
GracePeriodStatus type, String domainRepoId, DateTime expirationTime, String clientId) {
|
||||
return createInternal(type, domainRepoId, expirationTime, clientId, null, null);
|
||||
}
|
||||
|
||||
/** Constructs a GracePeriod of the given type from the provided one-time BillingEvent. */
|
||||
public static GracePeriod forBillingEvent(
|
||||
GracePeriodStatus type, BillingEvent.OneTime billingEvent) {
|
||||
GracePeriodStatus type, String domainRepoId, BillingEvent.OneTime billingEvent) {
|
||||
return create(
|
||||
type, billingEvent.getBillingTime(), billingEvent.getClientId(), Key.create(billingEvent));
|
||||
type,
|
||||
domainRepoId,
|
||||
billingEvent.getBillingTime(),
|
||||
billingEvent.getClientId(),
|
||||
billingEvent.createVKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of this {@link GracePeriod} with {@link #domainRepoId} set to the given value.
|
||||
*
|
||||
* <p>TODO(b/162739503): Remove this function after fully migrating to Cloud SQL.
|
||||
*/
|
||||
public GracePeriod cloneWithDomainRepoId(String domainRepoId) {
|
||||
GracePeriod clone = clone(this);
|
||||
clone.domainRepoId = checkArgumentNotNull(domainRepoId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2020 The Nomulus 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.model.domain;
|
||||
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingEvent.OneTime;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.MappedSuperclass;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Base class containing common fields and methods for {@link GracePeriod}. */
|
||||
@Embed
|
||||
@MappedSuperclass
|
||||
public class GracePeriodBase extends ImmutableObject {
|
||||
|
||||
/** Unique id required for hibernate representation. */
|
||||
@javax.persistence.Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Ignore
|
||||
Long id;
|
||||
|
||||
/** Repository id for the domain which this grace period belongs to. */
|
||||
@Ignore
|
||||
@Column(nullable = false)
|
||||
String domainRepoId;
|
||||
|
||||
/** The type of grace period. */
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
GracePeriodStatus type;
|
||||
|
||||
/** When the grace period ends. */
|
||||
@Column(nullable = false)
|
||||
DateTime expirationTime;
|
||||
|
||||
/** The registrar to bill. */
|
||||
@Column(name = "registrarId", nullable = false)
|
||||
String clientId;
|
||||
|
||||
/**
|
||||
* The one-time billing event corresponding to the action that triggered this grace period, or
|
||||
* null if not applicable. Not set for autorenew grace periods (which instead use the field {@code
|
||||
* billingEventRecurring}) or for redemption grace periods (since deletes have no cost).
|
||||
*/
|
||||
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
|
||||
@Column(name = "billing_event_id")
|
||||
VKey<OneTime> billingEventOneTime = null;
|
||||
|
||||
/**
|
||||
* The recurring billing event corresponding to the action that triggered this grace period, if
|
||||
* applicable - i.e. if the action was an autorenew - or null in all other cases.
|
||||
*/
|
||||
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
|
||||
@Column(name = "billing_recurrence_id")
|
||||
VKey<BillingEvent.Recurring> billingEventRecurring = null;
|
||||
|
||||
public GracePeriodStatus getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getDomainRepoId() {
|
||||
return domainRepoId;
|
||||
}
|
||||
|
||||
public DateTime getExpirationTime() {
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
/** Returns true if this GracePeriod has an associated BillingEvent; i.e. if it's refundable. */
|
||||
public boolean hasBillingEvent() {
|
||||
return billingEventOneTime != null || billingEventRecurring != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the one time billing event. The value will only be non-null if the type of this grace
|
||||
* period is not AUTO_RENEW.
|
||||
*/
|
||||
public VKey<BillingEvent.OneTime> getOneTimeBillingEvent() {
|
||||
return billingEventOneTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the recurring billing event. The value will only be non-null if the type of this grace
|
||||
* period is AUTO_RENEW.
|
||||
*/
|
||||
public VKey<BillingEvent.Recurring> getRecurringBillingEvent() {
|
||||
return billingEventRecurring;
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,14 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Range;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Embed;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Index;
|
||||
import com.googlecode.objectify.annotation.Mapify;
|
||||
import com.googlecode.objectify.annotation.OnLoad;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.domain.DomainFlowUtils;
|
||||
import google.registry.model.BackupGroupRoot;
|
||||
@@ -108,9 +110,22 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
|
||||
*/
|
||||
double discountFraction;
|
||||
|
||||
/** Whether the discount fraction (if any) also applies to premium names. Defaults to false. */
|
||||
boolean discountPremiums;
|
||||
|
||||
/** Up to how many years of initial creation receive the discount (if any). Defaults to 1. */
|
||||
int discountYears = 1;
|
||||
|
||||
/** The type of the token, either single-use or unlimited-use. */
|
||||
// TODO(b/130301183): this should not be nullable, we can remove this once we're sure it isn't
|
||||
@Nullable TokenType tokenType;
|
||||
TokenType tokenType;
|
||||
|
||||
// TODO: Remove onLoad once all allocation tokens are migrated to have a discountYears of 1.
|
||||
@OnLoad
|
||||
void onLoad() {
|
||||
if (discountYears == 0) {
|
||||
discountYears = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Promotional token validity periods.
|
||||
@@ -146,8 +161,8 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
|
||||
return token;
|
||||
}
|
||||
|
||||
public Key<HistoryEntry> getRedemptionHistoryEntry() {
|
||||
return redemptionHistoryEntry;
|
||||
public Optional<Key<HistoryEntry>> getRedemptionHistoryEntry() {
|
||||
return Optional.ofNullable(redemptionHistoryEntry);
|
||||
}
|
||||
|
||||
public boolean isRedeemed() {
|
||||
@@ -174,6 +189,16 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
|
||||
return discountFraction;
|
||||
}
|
||||
|
||||
public boolean shouldDiscountPremiums() {
|
||||
return discountPremiums;
|
||||
}
|
||||
|
||||
public int getDiscountYears() {
|
||||
// Allocation tokens created prior to the addition of the discountYears field will have a value
|
||||
// of 0 for it, but it should be the default value of 1 to retain the previous behavior.
|
||||
return Math.max(1, discountYears);
|
||||
}
|
||||
|
||||
public TokenType getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
@@ -193,6 +218,7 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
|
||||
|
||||
/** A builder for constructing {@link AllocationToken} objects, since they are immutable. */
|
||||
public static class Builder extends Buildable.Builder<AllocationToken> {
|
||||
|
||||
public Builder() {}
|
||||
|
||||
private Builder(AllocationToken instance) {
|
||||
@@ -210,6 +236,12 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
|
||||
getInstance().redemptionHistoryEntry == null
|
||||
|| TokenType.SINGLE_USE.equals(getInstance().tokenType),
|
||||
"Redemption history entry can only be specified for SINGLE_USE tokens");
|
||||
checkArgument(
|
||||
getInstance().discountFraction > 0 || !getInstance().discountPremiums,
|
||||
"Discount premiums can only be specified along with a discount fraction");
|
||||
checkArgument(
|
||||
getInstance().discountFraction > 0 || getInstance().discountYears == 1,
|
||||
"Discount years can only be specified along with a discount fraction");
|
||||
if (getInstance().domainName != null) {
|
||||
try {
|
||||
DomainFlowUtils.validateDomainName(getInstance().domainName);
|
||||
@@ -258,10 +290,26 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
|
||||
}
|
||||
|
||||
public Builder setDiscountFraction(double discountFraction) {
|
||||
checkArgument(
|
||||
Range.closed(0.0d, 1.0d).contains(discountFraction),
|
||||
"Discount fraction must be between 0 and 1 inclusive");
|
||||
getInstance().discountFraction = discountFraction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDiscountPremiums(boolean discountPremiums) {
|
||||
getInstance().discountPremiums = discountPremiums;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDiscountYears(int discountYears) {
|
||||
checkArgument(
|
||||
Range.closed(1, 10).contains(discountYears),
|
||||
"Discount years must be between 1 and 10 inclusive");
|
||||
getInstance().discountYears = discountYears;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTokenType(TokenType tokenType) {
|
||||
checkState(getInstance().tokenType == null, "Token type can only be set once");
|
||||
getInstance().tokenType = tokenType;
|
||||
|
||||
@@ -155,15 +155,7 @@ public abstract class PollMessage extends ImmutableObject
|
||||
public abstract VKey<? extends PollMessage> createVKey();
|
||||
|
||||
public static VKey<PollMessage> createVKey(Key<PollMessage> key) {
|
||||
// TODO(b/159207551): As it stands, the SQL key generated here doesn't mesh with the primary key
|
||||
// type for the table, which is a single long integer. Also note that the class id is not
|
||||
// correct here and as such the resulting key will not be loadable from SQL.
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
String path =
|
||||
key.getParent().getParent().getName() + "/" + key.getParent().getId() + key.getId();
|
||||
return VKey.create(PollMessage.class, path, key);
|
||||
return VKey.create(PollMessage.class, key.getId(), key);
|
||||
}
|
||||
|
||||
/** Override Buildable.asBuilder() to give this method stronger typing. */
|
||||
|
||||
@@ -48,6 +48,10 @@ public final class RdeRevision extends ImmutableObject {
|
||||
*/
|
||||
int revision;
|
||||
|
||||
public int getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next revision ID to use when staging a new deposit file for the given triplet.
|
||||
*
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
package google.registry.model.registry.label;
|
||||
|
||||
import static com.google.common.base.Charsets.US_ASCII;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.hash.Funnels.stringFunnel;
|
||||
import static com.google.common.hash.Funnels.unencodedCharsFunnel;
|
||||
import static google.registry.config.RegistryConfig.getDomainLabelListCacheDuration;
|
||||
import static google.registry.config.RegistryConfig.getSingletonCachePersistDuration;
|
||||
@@ -32,43 +34,82 @@ import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.hash.BloomFilter;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.annotation.Entity;
|
||||
import com.googlecode.objectify.annotation.Id;
|
||||
import com.googlecode.objectify.annotation.Ignore;
|
||||
import com.googlecode.objectify.annotation.Parent;
|
||||
import google.registry.model.Buildable;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.annotations.ReportedOn;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.schema.replay.DatastoreAndSqlEntity;
|
||||
import google.registry.schema.replay.DatastoreEntity;
|
||||
import google.registry.schema.replay.SqlEntity;
|
||||
import google.registry.schema.tld.PremiumListDao;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.CollectionTable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.ElementCollection;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.PostLoad;
|
||||
import javax.persistence.PrePersist;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Transient;
|
||||
import org.hibernate.LazyInitializationException;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.Money;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/** A premium list entity, persisted to Datastore, that is used to check domain label prices. */
|
||||
/**
|
||||
* A premium list entity that is used to check domain label prices.
|
||||
*
|
||||
* <p>Note that the primary key of this entity is {@link #revisionId}, which is auto-generated by
|
||||
* the database. So, if a retry of insertion happens after the previous attempt unexpectedly
|
||||
* succeeds, we will end up with having two exact same premium lists that differ only by revisionId.
|
||||
* This is fine though, because we only use the list with the highest revisionId.
|
||||
*/
|
||||
@ReportedOn
|
||||
@Entity
|
||||
@javax.persistence.Entity
|
||||
@Table(indexes = {@Index(columnList = "name", name = "premiumlist_name_idx")})
|
||||
public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.PremiumListEntry>
|
||||
implements DatastoreEntity {
|
||||
implements DatastoreAndSqlEntity {
|
||||
|
||||
/** Stores the revision key for the set of currently used premium list entry entities. */
|
||||
Key<PremiumListRevision> revisionKey;
|
||||
@Transient Key<PremiumListRevision> revisionKey;
|
||||
|
||||
@Override
|
||||
public ImmutableList<SqlEntity> toSqlEntities() {
|
||||
return ImmutableList.of(); // PremiumList is dual-written
|
||||
}
|
||||
@Ignore
|
||||
@Column(nullable = false)
|
||||
CurrencyUnit currency;
|
||||
|
||||
@Ignore
|
||||
@ElementCollection
|
||||
@CollectionTable(
|
||||
name = "PremiumEntry",
|
||||
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
|
||||
@MapKeyColumn(name = "domainLabel")
|
||||
@Column(name = "price", nullable = false)
|
||||
Map<String, BigDecimal> labelsToPrices;
|
||||
|
||||
@Ignore
|
||||
@Column(nullable = false)
|
||||
BloomFilter<String> bloomFilter;
|
||||
|
||||
/** Virtual parent entity for premium list entry entities associated with a single revision. */
|
||||
@ReportedOn
|
||||
@@ -247,6 +288,35 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
|
||||
return Optional.ofNullable(loadPremiumList(name));
|
||||
}
|
||||
|
||||
/** Returns the {@link CurrencyUnit} used for this list. */
|
||||
public CurrencyUnit getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Map} of domain labels to prices.
|
||||
*
|
||||
* <p>Note that this is lazily loaded and thus will throw a {@link LazyInitializationException} if
|
||||
* used outside the transaction in which the given entity was loaded. You generally should not be
|
||||
* using this anyway as it's inefficient to load all of the PremiumEntry rows if you don't need
|
||||
* them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
|
||||
*/
|
||||
@Nullable
|
||||
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
return labelsToPrices == null ? null : ImmutableMap.copyOf(labelsToPrices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Bloom filter to determine whether a label might be premium, or is definitely not.
|
||||
*
|
||||
* <p>If the domain label might be premium, then the next step is to check for the existence of a
|
||||
* corresponding row in the PremiumListEntry table. Otherwise, we know for sure it's not premium,
|
||||
* and no DB load is required.
|
||||
*/
|
||||
public BloomFilter<String> getBloomFilter() {
|
||||
return bloomFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* A premium list entry entity, persisted to Datastore. Each instance represents the price of a
|
||||
* single label on a given TLD.
|
||||
@@ -339,9 +409,39 @@ public final class PremiumList extends BaseDomainLabelList<Money, PremiumList.Pr
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCurrency(CurrencyUnit currency) {
|
||||
getInstance().currency = currency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLabelsToPrices(Map<String, BigDecimal> labelsToPrices) {
|
||||
getInstance().labelsToPrices = ImmutableMap.copyOf(labelsToPrices);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PremiumList build() {
|
||||
if (getInstance().labelsToPrices != null) {
|
||||
// ASCII is used for the charset because all premium list domain labels are stored
|
||||
// punycoded.
|
||||
getInstance().bloomFilter =
|
||||
BloomFilter.create(stringFunnel(US_ASCII), getInstance().labelsToPrices.size());
|
||||
getInstance()
|
||||
.labelsToPrices
|
||||
.keySet()
|
||||
.forEach(label -> getInstance().bloomFilter.put(label));
|
||||
}
|
||||
return super.build();
|
||||
}
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
void prePersist() {
|
||||
lastUpdateTime = creationTime;
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
void postLoad() {
|
||||
creationTime = lastUpdateTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ public enum ReservationType {
|
||||
ALLOWED_IN_SUNRISE("Reserved", 0),
|
||||
|
||||
/** The domain can only be registered by providing a specific token. */
|
||||
RESERVED_FOR_SPECIFIC_USE("Allocation token required", 1),
|
||||
RESERVED_FOR_SPECIFIC_USE("Reserved; alloc. token required", 1),
|
||||
|
||||
/** The domain is for an anchor tenant and can only be registered using a specific token. */
|
||||
RESERVED_FOR_ANCHOR_TENANT("Allocation token required", 2),
|
||||
RESERVED_FOR_ANCHOR_TENANT("Reserved; alloc. token required", 2),
|
||||
|
||||
/**
|
||||
* The domain can only be registered during sunrise for defensive purposes, and will never
|
||||
|
||||
@@ -140,6 +140,16 @@ public class Spec11ThreatMatch extends ImmutableObject implements Buildable, Sql
|
||||
return super.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually set the ID for testing or other special circumstances.
|
||||
*
|
||||
* <p>In general the ID is generated by SQL and there should be no need to set it manually.
|
||||
*/
|
||||
public Builder setId(Long id) {
|
||||
getInstance().id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDomainName(String domainName) {
|
||||
getInstance().domainName = domainName;
|
||||
getInstance().tld = DomainNameUtils.getTldFromDomainName(domainName);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2019 The Nomulus 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.persistence;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.function.Predicate;
|
||||
import javax.persistence.OptimisticLockException;
|
||||
import org.hibernate.exception.JDBCConnectionException;
|
||||
|
||||
/** Helpers for identifying retriable database operations. */
|
||||
public final class JpaRetries {
|
||||
|
||||
private JpaRetries() {}
|
||||
|
||||
private static final ImmutableSet<String> RETRIABLE_TXN_SQL_STATE =
|
||||
ImmutableSet.of(
|
||||
"40001", // serialization_failure
|
||||
"40P01", // deadlock_detected, PSQL-specific
|
||||
"55006", // object_in_use, PSQL and DB2
|
||||
"55P03" // lock_not_available, PSQL-specific
|
||||
);
|
||||
|
||||
private static final Predicate<Throwable> RETRIABLE_TXN_PREDICATE =
|
||||
Predicates.or(
|
||||
OptimisticLockException.class::isInstance,
|
||||
e ->
|
||||
e instanceof SQLException
|
||||
&& RETRIABLE_TXN_SQL_STATE.contains(((SQLException) e).getSQLState()));
|
||||
|
||||
private static final Predicate<Throwable> RETRIABLE_QUERY_PREDICATE =
|
||||
Predicates.or(
|
||||
JDBCConnectionException.class::isInstance,
|
||||
e ->
|
||||
e instanceof SQLException
|
||||
&& RETRIABLE_TXN_SQL_STATE.contains(((SQLException) e).getSQLState()));
|
||||
|
||||
public static boolean isFailedTxnRetriable(Throwable throwable) {
|
||||
Throwable t = throwable;
|
||||
while (t != null) {
|
||||
if (RETRIABLE_TXN_PREDICATE.test(t)) {
|
||||
return true;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isFailedQueryRetriable(Throwable throwable) {
|
||||
// TODO(weiminyu): check for more error codes.
|
||||
Throwable t = throwable;
|
||||
while (t != null) {
|
||||
if (RETRIABLE_QUERY_PREDICATE.test(t)) {
|
||||
return true;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
package google.registry.persistence;
|
||||
|
||||
import google.registry.persistence.converter.IntervalDescriptor;
|
||||
import google.registry.persistence.converter.StringCollectionDescriptor;
|
||||
import google.registry.persistence.converter.StringMapDescriptor;
|
||||
import java.sql.Types;
|
||||
@@ -30,6 +31,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
|
||||
registerColumnType(StringMapDescriptor.COLUMN_TYPE, StringMapDescriptor.COLUMN_NAME);
|
||||
registerColumnType(
|
||||
StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME);
|
||||
registerColumnType(IntervalDescriptor.COLUMN_TYPE, IntervalDescriptor.COLUMN_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -40,5 +42,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
|
||||
typeContributions.contributeSqlTypeDescriptor(StringCollectionDescriptor.getInstance());
|
||||
typeContributions.contributeJavaTypeDescriptor(StringMapDescriptor.getInstance());
|
||||
typeContributions.contributeSqlTypeDescriptor(StringMapDescriptor.getInstance());
|
||||
typeContributions.contributeJavaTypeDescriptor(IntervalDescriptor.getInstance());
|
||||
typeContributions.contributeSqlTypeDescriptor(IntervalDescriptor.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,24 +14,67 @@
|
||||
|
||||
package google.registry.persistence.converter;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.AttributeConverter;
|
||||
import javax.persistence.Converter;
|
||||
import org.joda.time.Duration;
|
||||
import org.joda.time.Period;
|
||||
import org.postgresql.util.PGInterval;
|
||||
|
||||
/** JPA converter to for storing/retrieving {@link org.joda.time.DateTime} objects. */
|
||||
/**
|
||||
* JPA converter to for storing/retrieving {@link org.joda.time.Duration} objects.
|
||||
*
|
||||
* <p>The Joda Time Duration is simply a number of milliseconds representing a length of time. This
|
||||
* can be converted into a PGInterval, but only for the fields that have a standard number of
|
||||
* milliseconds. Therefore, there is no way to populate the months or years field of a PGInterval
|
||||
* and be confident that it is representing the exact number of milliseconds it was intended to
|
||||
* represent.
|
||||
*/
|
||||
@Converter(autoApply = true)
|
||||
public class DurationConverter implements AttributeConverter<Duration, Long> {
|
||||
public class DurationConverter implements AttributeConverter<Duration, PGInterval> {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Long convertToDatabaseColumn(@Nullable Duration duration) {
|
||||
return duration == null ? null : duration.getMillis();
|
||||
public PGInterval convertToDatabaseColumn(@Nullable Duration duration) {
|
||||
if (duration == null) {
|
||||
return new PGInterval();
|
||||
}
|
||||
PGInterval interval = new PGInterval();
|
||||
Period period = new Period(duration);
|
||||
// For some reason when the period is created from the duration, it does not set days, but
|
||||
// instead just a total number of hours. Years and months are not created because those can
|
||||
// differ in length of milliseconds.
|
||||
interval.setDays(period.getHours() / 24);
|
||||
interval.setHours(period.getHours() % 24);
|
||||
interval.setMinutes(period.getMinutes());
|
||||
double millis = (double) period.getMillis() / 1000;
|
||||
interval.setSeconds(period.getSeconds() + millis);
|
||||
return interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Duration convertToEntityAttribute(@Nullable Long dbData) {
|
||||
return dbData == null ? null : new Duration(dbData);
|
||||
public Duration convertToEntityAttribute(@Nullable PGInterval dbData) {
|
||||
if (dbData == null) {
|
||||
return null;
|
||||
}
|
||||
PGInterval interval = null;
|
||||
try {
|
||||
interval = new PGInterval(dbData.toString());
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (interval.equals(new PGInterval())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int days = interval.getDays();
|
||||
final int hours = interval.getHours();
|
||||
final int mins = interval.getMinutes();
|
||||
final int secs = (int) interval.getSeconds();
|
||||
final int millis = interval.getMicroSeconds() / 1000;
|
||||
return new Period(0, 0, 0, days, hours, mins, secs, millis).toStandardDuration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright 2020 The Nomulus 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.persistence.converter;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import org.hibernate.type.descriptor.ValueBinder;
|
||||
import org.hibernate.type.descriptor.ValueExtractor;
|
||||
import org.hibernate.type.descriptor.WrapperOptions;
|
||||
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor;
|
||||
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
|
||||
import org.hibernate.type.descriptor.spi.JdbcRecommendedSqlTypeMappingContext;
|
||||
import org.hibernate.type.descriptor.sql.BasicBinder;
|
||||
import org.hibernate.type.descriptor.sql.BasicExtractor;
|
||||
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
|
||||
import org.postgresql.util.PGInterval;
|
||||
|
||||
/**
|
||||
* The {@link JavaTypeDescriptor} and {@link SqlTypeDescriptor} for {@link PGInterval}.
|
||||
*
|
||||
* @see <a
|
||||
* href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#basic-jpa-convert">JPA
|
||||
* 2.1 AttributeConverters</a>
|
||||
*/
|
||||
public class IntervalDescriptor extends AbstractTypeDescriptor<PGInterval>
|
||||
implements SqlTypeDescriptor {
|
||||
public static final int COLUMN_TYPE = Types.JAVA_OBJECT;
|
||||
public static final String COLUMN_NAME = "interval";
|
||||
private static final IntervalDescriptor INSTANCE = new IntervalDescriptor();
|
||||
|
||||
private IntervalDescriptor() {
|
||||
super(PGInterval.class);
|
||||
}
|
||||
|
||||
public static IntervalDescriptor getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PGInterval fromString(String string) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Constructing IntervalDescriptor from string is not allowed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> X unwrap(PGInterval value, Class<X> type, WrapperOptions options) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (PGInterval.class.isAssignableFrom(type)) {
|
||||
return (X) value;
|
||||
}
|
||||
throw unknownUnwrap(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> PGInterval wrap(X value, WrapperOptions options) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof PGInterval) {
|
||||
try {
|
||||
return new PGInterval(value.toString());
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
throw unknownWrap(value.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSqlType() {
|
||||
return COLUMN_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlTypeDescriptor getJdbcRecommendedSqlType(JdbcRecommendedSqlTypeMappingContext context) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBeRemapped() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {
|
||||
return new BasicBinder<X>(javaTypeDescriptor, this) {
|
||||
@Override
|
||||
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
|
||||
throws SQLException {
|
||||
st.setObject(index, new PGInterval(value.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
|
||||
throws SQLException {
|
||||
st.setObject(name, new PGInterval(value.toString()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) {
|
||||
return new BasicExtractor<X>(javaTypeDescriptor, this) {
|
||||
@Override
|
||||
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
|
||||
return javaTypeDescriptor.wrap(rs.getObject(name), options);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected X doExtract(CallableStatement statement, int index, WrapperOptions options)
|
||||
throws SQLException {
|
||||
return javaTypeDescriptor.wrap(statement.getObject(index), options);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected X doExtract(CallableStatement statement, String name, WrapperOptions options)
|
||||
throws SQLException {
|
||||
return javaTypeDescriptor.wrap(statement.getObject(name), options);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
package google.registry.persistence.transaction;
|
||||
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.function.Supplier;
|
||||
import javax.persistence.EntityManager;
|
||||
|
||||
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
|
||||
@@ -23,6 +24,12 @@ public interface JpaTransactionManager extends TransactionManager {
|
||||
/** Returns the {@link EntityManager} for the current request. */
|
||||
EntityManager getEntityManager();
|
||||
|
||||
/** Executes the work in a transaction with no retries and returns the result. */
|
||||
<T> T transactNoRetry(Supplier<T> work);
|
||||
|
||||
/** Executes the work in a transaction with no retries. */
|
||||
void transactNoRetry(Runnable work);
|
||||
|
||||
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
|
||||
public abstract <T> void assertDelete(VKey<T> key);
|
||||
|
||||
|
||||
@@ -26,8 +26,12 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SystemSleeper;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.NoSuchElementException;
|
||||
@@ -48,6 +52,7 @@ import org.joda.time.DateTime;
|
||||
public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final Retrier retrier = new Retrier(new SystemSleeper(), 3);
|
||||
|
||||
// EntityManagerFactory is thread safe.
|
||||
private final EntityManagerFactory emf;
|
||||
@@ -93,6 +98,40 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
public <T> T transact(Supplier<T> work) {
|
||||
// TODO(shicong): Investigate removing transactNew functionality after migration as it may
|
||||
// be same as this one.
|
||||
return retrier.callWithRetry(
|
||||
() -> {
|
||||
if (inTransaction()) {
|
||||
return work.get();
|
||||
}
|
||||
TransactionInfo txnInfo = transactionInfo.get();
|
||||
txnInfo.entityManager = emf.createEntityManager();
|
||||
EntityTransaction txn = txnInfo.entityManager.getTransaction();
|
||||
try {
|
||||
txn.begin();
|
||||
txnInfo.start(clock);
|
||||
T result = work.get();
|
||||
txnInfo.recordTransaction();
|
||||
txn.commit();
|
||||
return result;
|
||||
} catch (RuntimeException | Error e) {
|
||||
// Error is unchecked!
|
||||
try {
|
||||
txn.rollback();
|
||||
logger.atWarning().log("Error during transaction; transaction rolled back");
|
||||
} catch (Throwable rollbackException) {
|
||||
logger.atSevere().withCause(rollbackException).log(
|
||||
"Rollback failed; suppressing error");
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
txnInfo.clear();
|
||||
}
|
||||
},
|
||||
JpaRetries::isFailedTxnRetriable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNoRetry(Supplier<T> work) {
|
||||
if (inTransaction()) {
|
||||
return work.get();
|
||||
}
|
||||
@@ -101,9 +140,9 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
EntityTransaction txn = txnInfo.entityManager.getTransaction();
|
||||
try {
|
||||
txn.begin();
|
||||
txnInfo.inTransaction = true;
|
||||
txnInfo.transactionTime = clock.nowUtc();
|
||||
txnInfo.start(clock);
|
||||
T result = work.get();
|
||||
txnInfo.recordTransaction();
|
||||
txn.commit();
|
||||
return result;
|
||||
} catch (RuntimeException | Error e) {
|
||||
@@ -129,6 +168,15 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transactNoRetry(Runnable work) {
|
||||
transactNoRetry(
|
||||
() -> {
|
||||
work.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T transactNew(Supplier<T> work) {
|
||||
return transact(work);
|
||||
@@ -141,11 +189,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
@Override
|
||||
public <T> T transactNewReadOnly(Supplier<T> work) {
|
||||
return transact(
|
||||
() -> {
|
||||
getEntityManager().createNativeQuery("SET TRANSACTION READ ONLY").executeUpdate();
|
||||
return work.get();
|
||||
});
|
||||
return retrier.callWithRetry(
|
||||
() ->
|
||||
transact(
|
||||
() -> {
|
||||
getEntityManager().createNativeQuery("SET TRANSACTION READ ONLY").executeUpdate();
|
||||
return work.get();
|
||||
}),
|
||||
JpaRetries::isFailedQueryRetriable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,7 +210,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
|
||||
@Override
|
||||
public <T> T doTransactionless(Supplier<T> work) {
|
||||
return transact(work);
|
||||
return retrier.callWithRetry(() -> transact(work), JpaRetries::isFailedQueryRetriable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,6 +228,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
checkArgumentNotNull(entity, "entity must be specified");
|
||||
assertInTransaction();
|
||||
getEntityManager().persist(entity);
|
||||
transactionInfo.get().addUpdate(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,6 +243,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
checkArgumentNotNull(entity, "entity must be specified");
|
||||
assertInTransaction();
|
||||
getEntityManager().merge(entity);
|
||||
transactionInfo.get().addUpdate(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,6 +259,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
assertInTransaction();
|
||||
checkArgument(checkExists(entity), "Given entity does not exist");
|
||||
getEntityManager().merge(entity);
|
||||
transactionInfo.get().addUpdate(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -297,6 +351,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
String.format("DELETE FROM %s WHERE %s", entityType.getName(), getAndClause(entityIds));
|
||||
Query query = getEntityManager().createQuery(sql);
|
||||
entityIds.forEach(entityId -> query.setParameter(entityId.name, entityId.value));
|
||||
transactionInfo.get().addDelete(key);
|
||||
return query.executeUpdate();
|
||||
}
|
||||
|
||||
@@ -387,9 +442,23 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
boolean inTransaction = false;
|
||||
DateTime transactionTime;
|
||||
|
||||
// Serializable representation of the transaction to be persisted in the Transaction table.
|
||||
Transaction.Builder contentsBuilder;
|
||||
|
||||
/** Start a new transaction. */
|
||||
private void start(Clock clock) {
|
||||
checkArgumentNotNull(clock);
|
||||
inTransaction = true;
|
||||
transactionTime = clock.nowUtc();
|
||||
if (RegistryConfig.getCloudSqlReplicateTransactions()) {
|
||||
contentsBuilder = new Transaction.Builder();
|
||||
}
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
inTransaction = false;
|
||||
transactionTime = null;
|
||||
contentsBuilder = null;
|
||||
if (entityManager != null) {
|
||||
// Close this EntityManager just let the connection pool be able to reuse it, it doesn't
|
||||
// close the underlying database connection.
|
||||
@@ -397,5 +466,26 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
entityManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void addUpdate(Object entity) {
|
||||
if (contentsBuilder != null) {
|
||||
contentsBuilder.addUpdate(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private void addDelete(VKey<?> key) {
|
||||
if (contentsBuilder != null) {
|
||||
contentsBuilder.addDelete(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordTransaction() {
|
||||
if (contentsBuilder != null) {
|
||||
Transaction persistedTxn = contentsBuilder.build();
|
||||
if (!persistedTxn.isEmpty()) {
|
||||
entityManager.persist(persistedTxn.toEntity());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +109,20 @@ public class Transaction extends ImmutableObject implements Buildable {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Returns true if the transaction contains no mutations. */
|
||||
public boolean isEmpty() {
|
||||
return mutations.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
}
|
||||
|
||||
public final TransactionEntity toEntity() {
|
||||
return new TransactionEntity(serialize());
|
||||
}
|
||||
|
||||
public static class Builder extends GenericBuilder<Transaction, Builder> {
|
||||
|
||||
ImmutableList.Builder listBuilder = new ImmutableList.Builder();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2020 The Nomulus 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.persistence.transaction;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
/**
|
||||
* Object to be stored in the transaction table.
|
||||
*
|
||||
* <p>This consists of a sequential identifier and a serialized {@code Tranaction} object.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "Transaction")
|
||||
public class TransactionEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
long id;
|
||||
|
||||
byte[] contents;
|
||||
|
||||
TransactionEntity() {}
|
||||
|
||||
TransactionEntity(byte[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public enum RdeResourceType {
|
||||
DOMAIN("urn:ietf:params:xml:ns:rdeDomain-1.0", EnumSet.of(FULL, THIN)),
|
||||
HOST("urn:ietf:params:xml:ns:rdeHost-1.0", EnumSet.of(FULL)),
|
||||
REGISTRAR("urn:ietf:params:xml:ns:rdeRegistrar-1.0", EnumSet.of(FULL, THIN)),
|
||||
IDN("urn:ietf:params:xml:ns:rdeIDN-1.0", EnumSet.of(FULL, THIN)),
|
||||
IDN("urn:ietf:params:xml:ns:rdeIDN-1.0", EnumSet.of(FULL)),
|
||||
HEADER("urn:ietf:params:xml:ns:rdeHeader-1.0", EnumSet.of(FULL, THIN));
|
||||
|
||||
private final String uri;
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
|
||||
private final byte[] stagingKeyBytes;
|
||||
private final RdeMarshaller marshaller;
|
||||
|
||||
private RdeStagingReducer(
|
||||
RdeStagingReducer(
|
||||
TaskQueueUtils taskQueueUtils,
|
||||
LockHandler lockHandler,
|
||||
int gcsBufferSize,
|
||||
@@ -125,7 +125,7 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
|
||||
final DateTime watermark = key.watermark();
|
||||
final int revision =
|
||||
Optional.ofNullable(key.revision())
|
||||
.orElse(RdeRevision.getNextRevision(tld, watermark, mode));
|
||||
.orElseGet(() -> RdeRevision.getNextRevision(tld, watermark, mode));
|
||||
String id = RdeUtil.timestampToId(watermark);
|
||||
String prefix = RdeNamingUtils.makeRydeFilename(tld, watermark, mode, 1, revision);
|
||||
if (key.manual()) {
|
||||
@@ -168,9 +168,13 @@ public final class RdeStagingReducer extends Reducer<PendingDeposit, DepositFrag
|
||||
logger.atSevere().log("Fragment error: %s", fragment.error());
|
||||
}
|
||||
}
|
||||
for (IdnTableEnum idn : IdnTableEnum.values()) {
|
||||
output.write(marshaller.marshalIdn(idn.getTable()));
|
||||
counter.increment(RdeResourceType.IDN);
|
||||
|
||||
// Don't write the IDN elements for BRDA.
|
||||
if (mode == RdeMode.FULL) {
|
||||
for (IdnTableEnum idn : IdnTableEnum.values()) {
|
||||
output.write(marshaller.marshalIdn(idn.getTable()));
|
||||
counter.increment(RdeResourceType.IDN);
|
||||
}
|
||||
}
|
||||
|
||||
// Output XML that says how many resources were emitted.
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.schema.tld;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.model.ImmutableObject;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.schema.replay.DatastoreEntity;
|
||||
import google.registry.schema.replay.SqlEntity;
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// Copyright 2019 The Nomulus 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.schema.tld;
|
||||
|
||||
import static com.google.common.base.Charsets.US_ASCII;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.hash.Funnels.stringFunnel;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.hash.BloomFilter;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.schema.replay.DatastoreEntity;
|
||||
import google.registry.schema.replay.SqlEntity;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.CollectionTable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.ElementCollection;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.MapKeyColumn;
|
||||
import javax.persistence.Table;
|
||||
import org.hibernate.LazyInitializationException;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* A list of premium prices for domain names.
|
||||
*
|
||||
* <p>Note that the primary key of this entity is {@link #revisionId}, which is auto-generated by
|
||||
* the database. So, if a retry of insertion happens after the previous attempt unexpectedly
|
||||
* succeeds, we will end up with having two exact same premium lists that differ only by revisionId.
|
||||
* This is fine though, because we only use the list with the highest revisionId.
|
||||
*/
|
||||
@Entity
|
||||
@Table(indexes = {@Index(columnList = "name", name = "premiumlist_name_idx")})
|
||||
public class PremiumList implements SqlEntity {
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(nullable = false)
|
||||
private Long revisionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);
|
||||
|
||||
@Column(nullable = false)
|
||||
private CurrencyUnit currency;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(
|
||||
name = "PremiumEntry",
|
||||
joinColumns = @JoinColumn(name = "revisionId", referencedColumnName = "revisionId"))
|
||||
@MapKeyColumn(name = "domainLabel")
|
||||
@Column(name = "price", nullable = false)
|
||||
private Map<String, BigDecimal> labelsToPrices;
|
||||
|
||||
@Column(nullable = false)
|
||||
private BloomFilter<String> bloomFilter;
|
||||
|
||||
private PremiumList(String name, CurrencyUnit currency, Map<String, BigDecimal> labelsToPrices) {
|
||||
this.name = name;
|
||||
this.currency = currency;
|
||||
this.labelsToPrices = labelsToPrices;
|
||||
// ASCII is used for the charset because all premium list domain labels are stored punycoded.
|
||||
this.bloomFilter = BloomFilter.create(stringFunnel(US_ASCII), labelsToPrices.size());
|
||||
labelsToPrices.keySet().forEach(this.bloomFilter::put);
|
||||
}
|
||||
|
||||
// Hibernate requires this default constructor.
|
||||
private PremiumList() {}
|
||||
|
||||
/** Constructs a {@link PremiumList} object. */
|
||||
public static PremiumList create(
|
||||
String name, CurrencyUnit currency, Map<String, BigDecimal> labelsToPrices) {
|
||||
return new PremiumList(name, currency, labelsToPrices);
|
||||
}
|
||||
|
||||
/** Returns the name of the premium list, which is usually also a TLD string. */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Returns the {@link CurrencyUnit} used for this list. */
|
||||
public CurrencyUnit getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
/** Returns the ID of this revision, or throws if null. */
|
||||
public Long getRevisionId() {
|
||||
checkState(
|
||||
revisionId != null,
|
||||
"revisionId is null because this object has not yet been persisted to the DB");
|
||||
return revisionId;
|
||||
}
|
||||
|
||||
/** Returns the creation time of this revision of the premium list. */
|
||||
public DateTime getCreationTimestamp() {
|
||||
return creationTimestamp.getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Map} of domain labels to prices.
|
||||
*
|
||||
* <p>Note that this is lazily loaded and thus will throw a {@link LazyInitializationException} if
|
||||
* used outside the transaction in which the given entity was loaded. You generally should not be
|
||||
* using this anyway as it's inefficient to load all of the PremiumEntry rows if you don't need
|
||||
* them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
|
||||
*/
|
||||
@Nullable
|
||||
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
return labelsToPrices == null ? null : ImmutableMap.copyOf(labelsToPrices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Bloom filter to determine whether a label might be premium, or is definitely not.
|
||||
*
|
||||
* <p>If the domain label might be premium, then the next step is to check for the existence of a
|
||||
* corresponding row in the PremiumListEntry table. Otherwise, we know for sure it's not premium,
|
||||
* and no DB load is required.
|
||||
*/
|
||||
public BloomFilter<String> getBloomFilter() {
|
||||
return bloomFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableList<DatastoreEntity> toDatastoreEntities() {
|
||||
return ImmutableList.of(); // PremiumList is dual-written
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -20,6 +20,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
|
||||
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.schema.tld.PremiumListCache.RevisionIdAndLabel;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.schema.tld;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -23,11 +24,13 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.model.registry.label.PremiumList.PremiumListEntry;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Static utility methods for {@link PremiumList}. */
|
||||
public class PremiumListUtils {
|
||||
@@ -37,10 +40,7 @@ public class PremiumListUtils {
|
||||
Splitter.on('\n').omitEmptyStrings().splitToList(inputData);
|
||||
|
||||
ImmutableMap<String, PremiumListEntry> prices =
|
||||
new google.registry.model.registry.label.PremiumList.Builder()
|
||||
.setName(name)
|
||||
.build()
|
||||
.parse(inputDataPreProcessed);
|
||||
new PremiumList.Builder().setName(name).build().parse(inputDataPreProcessed);
|
||||
ImmutableSet<CurrencyUnit> currencies =
|
||||
prices.values().stream()
|
||||
.map(e -> e.getValue().getCurrencyUnit())
|
||||
@@ -54,7 +54,12 @@ public class PremiumListUtils {
|
||||
|
||||
Map<String, BigDecimal> priceAmounts =
|
||||
Maps.transformValues(prices, ple -> ple.getValue().getAmount());
|
||||
return PremiumList.create(name, currency, priceAmounts);
|
||||
return new PremiumList.Builder()
|
||||
.setName(name)
|
||||
.setCurrency(currency)
|
||||
.setLabelsToPrices(priceAmounts)
|
||||
.setCreationTime(DateTime.now(UTC))
|
||||
.build();
|
||||
}
|
||||
|
||||
private PremiumListUtils() {}
|
||||
|
||||
@@ -15,20 +15,11 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.util.CollectionUtils.findDuplicates;
|
||||
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.template.soy.data.SoyListData;
|
||||
import com.google.template.soy.data.SoyMapData;
|
||||
import google.registry.tools.params.NameserversParameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@@ -80,77 +71,15 @@ abstract class CreateOrUpdateDomainCommand extends MutatingEppToolCommand {
|
||||
String password;
|
||||
|
||||
@Parameter(
|
||||
names = "--ds_records",
|
||||
description =
|
||||
"Comma-separated list of DS records. Each DS record is given as "
|
||||
+ "<keyTag> <alg> <digestType> <digest>, in order, as it appears in the Zonefile.",
|
||||
converter = DsRecordConverter.class
|
||||
)
|
||||
names = "--ds_records",
|
||||
description =
|
||||
"Comma-separated list of DS records. Each DS record is given as "
|
||||
+ "<keyTag> <alg> <digestType> <digest>, in order, as it appears in the Zonefile.",
|
||||
converter = DsRecord.Converter.class)
|
||||
List<DsRecord> dsRecords = new ArrayList<>();
|
||||
|
||||
Set<String> domains;
|
||||
|
||||
@AutoValue
|
||||
abstract static class DsRecord {
|
||||
private static final Splitter SPLITTER =
|
||||
Splitter.on(CharMatcher.whitespace()).omitEmptyStrings();
|
||||
|
||||
public abstract int keyTag();
|
||||
public abstract int alg();
|
||||
public abstract int digestType();
|
||||
public abstract String digest();
|
||||
|
||||
private static DsRecord create(int keyTag, int alg, int digestType, String digest) {
|
||||
digest = Ascii.toUpperCase(digest);
|
||||
checkArgument(
|
||||
BaseEncoding.base16().canDecode(digest),
|
||||
"digest should be even-lengthed hex, but is %s (length %s)",
|
||||
digest,
|
||||
digest.length());
|
||||
return new AutoValue_CreateOrUpdateDomainCommand_DsRecord(keyTag, alg, digestType, digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string representation of the DS record.
|
||||
*
|
||||
* <p>The string format accepted is "[keyTag] [alg] [digestType] [digest]" (i.e., the 4
|
||||
* arguments separated by any number of spaces, as it appears in the Zone file)
|
||||
*/
|
||||
public static DsRecord parse(String dsRecord) {
|
||||
List<String> elements = SPLITTER.splitToList(dsRecord);
|
||||
checkArgument(
|
||||
elements.size() == 4,
|
||||
"dsRecord %s should have 4 parts, but has %s",
|
||||
dsRecord,
|
||||
elements.size());
|
||||
return DsRecord.create(
|
||||
Integer.parseUnsignedInt(elements.get(0)),
|
||||
Integer.parseUnsignedInt(elements.get(1)),
|
||||
Integer.parseUnsignedInt(elements.get(2)),
|
||||
elements.get(3));
|
||||
}
|
||||
|
||||
public SoyMapData toSoyData() {
|
||||
return new SoyMapData(
|
||||
"keyTag", keyTag(),
|
||||
"alg", alg(),
|
||||
"digestType", digestType(),
|
||||
"digest", digest());
|
||||
}
|
||||
|
||||
public static SoyListData convertToSoy(List<DsRecord> dsRecords) {
|
||||
return new SoyListData(
|
||||
dsRecords.stream().map(DsRecord::toSoyData).collect(toImmutableList()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class DsRecordConverter implements IStringConverter<DsRecord> {
|
||||
@Override
|
||||
public DsRecord convert(String dsRecord) {
|
||||
return DsRecord.parse(dsRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initEppToolCommand() throws Exception {
|
||||
checkArgument(nameservers.size() <= 13, "There can be at most 13 nameservers.");
|
||||
|
||||
@@ -14,19 +14,69 @@
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import google.registry.beam.initsql.BeamJpaModule.JpaTransactionManagerComponent;
|
||||
import google.registry.beam.initsql.JpaSupplierFactory;
|
||||
import google.registry.beam.spec11.Spec11Pipeline;
|
||||
import google.registry.config.CredentialModule.LocalCredential;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.util.GoogleCredentialsBundle;
|
||||
import google.registry.util.Retrier;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Nomulus command that deploys the {@link Spec11Pipeline} template. */
|
||||
@Parameters(commandDescription = "Deploy the Spec11 pipeline to GCS.")
|
||||
public class DeploySpec11PipelineCommand implements Command {
|
||||
|
||||
@Inject Spec11Pipeline spec11Pipeline;
|
||||
@Inject
|
||||
@Config("projectId")
|
||||
String projectId;
|
||||
|
||||
@Parameter(
|
||||
names = {"-p", "--project"},
|
||||
description = "Cloud KMS project ID",
|
||||
required = true)
|
||||
String cloudKmsProjectId;
|
||||
|
||||
@Inject
|
||||
@Config("beamStagingUrl")
|
||||
String beamStagingUrl;
|
||||
|
||||
@Inject
|
||||
@Config("spec11TemplateUrl")
|
||||
String spec11TemplateUrl;
|
||||
|
||||
@Inject
|
||||
@Config("reportingBucketUrl")
|
||||
String reportingBucketUrl;
|
||||
|
||||
@Inject @LocalCredential GoogleCredentialsBundle googleCredentialsBundle;
|
||||
@Inject Retrier retrier;
|
||||
|
||||
@Inject
|
||||
@Nullable
|
||||
@Config("sqlAccessInfoFile")
|
||||
String sqlAccessInfoFile;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
spec11Pipeline.deploy();
|
||||
JpaSupplierFactory jpaSupplierFactory =
|
||||
new JpaSupplierFactory(
|
||||
sqlAccessInfoFile,
|
||||
cloudKmsProjectId,
|
||||
JpaTransactionManagerComponent::cloudSqlJpaTransactionManager);
|
||||
|
||||
Spec11Pipeline pipeline =
|
||||
new Spec11Pipeline(
|
||||
projectId,
|
||||
beamStagingUrl,
|
||||
spec11TemplateUrl,
|
||||
reportingBucketUrl,
|
||||
jpaSupplierFactory,
|
||||
googleCredentialsBundle,
|
||||
retrier);
|
||||
pipeline.deploy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ public final class DomainLockUtils {
|
||||
RegistryLock newLock =
|
||||
RegistryLockDao.save(lock.asBuilder().setLockCompletionTimestamp(now).build());
|
||||
setAsRelock(newLock);
|
||||
tm().transact(() -> applyLockStatuses(newLock, now));
|
||||
tm().transact(() -> applyLockStatuses(newLock, now, isAdmin));
|
||||
return newLock;
|
||||
});
|
||||
}
|
||||
@@ -171,7 +171,7 @@ public final class DomainLockUtils {
|
||||
createLockBuilder(domainName, registrarId, registrarPocId, isAdmin)
|
||||
.setLockCompletionTimestamp(now)
|
||||
.build());
|
||||
tm().transact(() -> applyLockStatuses(newLock, now));
|
||||
tm().transact(() -> applyLockStatuses(newLock, now, isAdmin));
|
||||
setAsRelock(newLock);
|
||||
return newLock;
|
||||
});
|
||||
@@ -222,18 +222,18 @@ public final class DomainLockUtils {
|
||||
String domainName, String registrarId, @Nullable String registrarPocId, boolean isAdmin) {
|
||||
DateTime now = jpaTm().getTransactionTime();
|
||||
DomainBase domainBase = getDomain(domainName, registrarId, now);
|
||||
verifyDomainNotLocked(domainBase);
|
||||
verifyDomainNotLocked(domainBase, isAdmin);
|
||||
|
||||
// Multiple pending actions are not allowed
|
||||
// Multiple pending actions are not allowed for non-admins
|
||||
RegistryLockDao.getMostRecentByRepoId(domainBase.getRepoId())
|
||||
.ifPresent(
|
||||
previousLock ->
|
||||
checkArgument(
|
||||
previousLock.isLockRequestExpired(now)
|
||||
|| previousLock.getUnlockCompletionTimestamp().isPresent(),
|
||||
|| previousLock.getUnlockCompletionTimestamp().isPresent()
|
||||
|| isAdmin,
|
||||
"A pending or completed lock action already exists for %s",
|
||||
previousLock.getDomainName()));
|
||||
|
||||
return new RegistryLock.Builder()
|
||||
.setVerificationCode(stringGenerator.createString(VERIFICATION_CODE_LENGTH))
|
||||
.setDomainName(domainName)
|
||||
@@ -250,6 +250,8 @@ public final class DomainLockUtils {
|
||||
Optional<RegistryLock> lockOptional =
|
||||
RegistryLockDao.getMostRecentVerifiedLockByRepoId(domainBase.getRepoId());
|
||||
|
||||
verifyDomainLocked(domainBase, isAdmin);
|
||||
|
||||
RegistryLock.Builder newLockBuilder;
|
||||
if (isAdmin) {
|
||||
// Admins should always be able to unlock domains in case we get in a bad state
|
||||
@@ -265,7 +267,6 @@ public final class DomainLockUtils {
|
||||
.setLockCompletionTimestamp(now)
|
||||
.setRegistrarId(registrarId));
|
||||
} else {
|
||||
verifyDomainLocked(domainBase);
|
||||
RegistryLock lock =
|
||||
lockOptional.orElseThrow(
|
||||
() ->
|
||||
@@ -293,16 +294,17 @@ public final class DomainLockUtils {
|
||||
.setRegistrarId(registrarId);
|
||||
}
|
||||
|
||||
private static void verifyDomainNotLocked(DomainBase domainBase) {
|
||||
private static void verifyDomainNotLocked(DomainBase domainBase, boolean isAdmin) {
|
||||
checkArgument(
|
||||
!domainBase.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES),
|
||||
isAdmin || !domainBase.getStatusValues().containsAll(REGISTRY_LOCK_STATUSES),
|
||||
"Domain %s is already locked",
|
||||
domainBase.getDomainName());
|
||||
}
|
||||
|
||||
private static void verifyDomainLocked(DomainBase domainBase) {
|
||||
private static void verifyDomainLocked(DomainBase domainBase, boolean isAdmin) {
|
||||
checkArgument(
|
||||
!Sets.intersection(domainBase.getStatusValues(), REGISTRY_LOCK_STATUSES).isEmpty(),
|
||||
isAdmin
|
||||
|| !Sets.intersection(domainBase.getStatusValues(), REGISTRY_LOCK_STATUSES).isEmpty(),
|
||||
"Domain %s is already unlocked",
|
||||
domainBase.getDomainName());
|
||||
}
|
||||
@@ -310,8 +312,7 @@ public final class DomainLockUtils {
|
||||
private DomainBase getDomain(String domainName, String registrarId, DateTime now) {
|
||||
DomainBase domain =
|
||||
loadByForeignKeyCached(DomainBase.class, domainName, now)
|
||||
.orElseThrow(
|
||||
() -> new IllegalArgumentException(String.format("Unknown domain %s", domainName)));
|
||||
.orElseThrow(() -> new IllegalArgumentException("Domain doesn't exist"));
|
||||
// The user must have specified either the correct registrar ID or the admin registrar ID
|
||||
checkArgument(
|
||||
registryAdminRegistrarId.equals(registrarId)
|
||||
@@ -330,9 +331,9 @@ public final class DomainLockUtils {
|
||||
String.format("Invalid verification code %s", verificationCode)));
|
||||
}
|
||||
|
||||
private void applyLockStatuses(RegistryLock lock, DateTime lockTime) {
|
||||
private void applyLockStatuses(RegistryLock lock, DateTime lockTime, boolean isAdmin) {
|
||||
DomainBase domain = getDomain(lock.getDomainName(), lock.getRegistrarId(), lockTime);
|
||||
verifyDomainNotLocked(domain);
|
||||
verifyDomainNotLocked(domain, isAdmin);
|
||||
|
||||
DomainBase newDomain =
|
||||
domain
|
||||
@@ -345,9 +346,7 @@ public final class DomainLockUtils {
|
||||
|
||||
private void removeLockStatuses(RegistryLock lock, boolean isAdmin, DateTime unlockTime) {
|
||||
DomainBase domain = getDomain(lock.getDomainName(), lock.getRegistrarId(), unlockTime);
|
||||
if (!isAdmin) {
|
||||
verifyDomainLocked(domain);
|
||||
}
|
||||
verifyDomainLocked(domain, isAdmin);
|
||||
|
||||
DomainBase newDomain =
|
||||
domain
|
||||
|
||||
90
core/src/main/java/google/registry/tools/DsRecord.java
Normal file
90
core/src/main/java/google/registry/tools/DsRecord.java
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2017 The Nomulus 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.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.template.soy.data.SoyListData;
|
||||
import com.google.template.soy.data.SoyMapData;
|
||||
import java.util.List;
|
||||
|
||||
@AutoValue
|
||||
abstract class DsRecord {
|
||||
private static final Splitter SPLITTER = Splitter.on(CharMatcher.whitespace()).omitEmptyStrings();
|
||||
|
||||
public abstract int keyTag();
|
||||
|
||||
public abstract int alg();
|
||||
|
||||
public abstract int digestType();
|
||||
|
||||
public abstract String digest();
|
||||
|
||||
private static DsRecord create(int keyTag, int alg, int digestType, String digest) {
|
||||
digest = Ascii.toUpperCase(digest);
|
||||
checkArgument(
|
||||
BaseEncoding.base16().canDecode(digest),
|
||||
"digest should be even-lengthed hex, but is %s (length %s)",
|
||||
digest,
|
||||
digest.length());
|
||||
return new AutoValue_DsRecord(keyTag, alg, digestType, digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string representation of the DS record.
|
||||
*
|
||||
* <p>The string format accepted is "[keyTag] [alg] [digestType] [digest]" (i.e., the 4 arguments
|
||||
* separated by any number of spaces, as it appears in the Zone file)
|
||||
*/
|
||||
public static DsRecord parse(String dsRecord) {
|
||||
List<String> elements = SPLITTER.splitToList(dsRecord);
|
||||
checkArgument(
|
||||
elements.size() == 4,
|
||||
"dsRecord %s should have 4 parts, but has %s",
|
||||
dsRecord,
|
||||
elements.size());
|
||||
return DsRecord.create(
|
||||
Integer.parseUnsignedInt(elements.get(0)),
|
||||
Integer.parseUnsignedInt(elements.get(1)),
|
||||
Integer.parseUnsignedInt(elements.get(2)),
|
||||
elements.get(3));
|
||||
}
|
||||
|
||||
public SoyMapData toSoyData() {
|
||||
return new SoyMapData(
|
||||
"keyTag", keyTag(),
|
||||
"alg", alg(),
|
||||
"digestType", digestType(),
|
||||
"digest", digest());
|
||||
}
|
||||
|
||||
public static SoyListData convertToSoy(List<DsRecord> dsRecords) {
|
||||
return new SoyListData(dsRecords.stream().map(DsRecord::toSoyData).collect(toImmutableList()));
|
||||
}
|
||||
|
||||
public static class Converter implements IStringConverter<DsRecord> {
|
||||
@Override
|
||||
public DsRecord convert(String dsRecord) {
|
||||
return DsRecord.parse(dsRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,20 @@ class GenerateAllocationTokensCommand implements CommandWithRemoteApi {
|
||||
description =
|
||||
"A discount off the base price for the first year between 0.0 and 1.0. Default is 0.0,"
|
||||
+ " i.e. no discount.")
|
||||
private double discountFraction;
|
||||
private Double discountFraction;
|
||||
|
||||
@Parameter(
|
||||
names = {"--discount_premiums"},
|
||||
description =
|
||||
"Whether the discount is valid for premium names in addition to standard ones. Default"
|
||||
+ " is false.",
|
||||
arity = 1)
|
||||
private Boolean discountPremiums;
|
||||
|
||||
@Parameter(
|
||||
names = {"--discount_years"},
|
||||
description = "The number of years the discount applies for. Default is 1, max value is 10.")
|
||||
private Integer discountYears;
|
||||
|
||||
@Parameter(
|
||||
names = "--token_status_transitions",
|
||||
@@ -170,8 +183,10 @@ class GenerateAllocationTokensCommand implements CommandWithRemoteApi {
|
||||
.setToken(t)
|
||||
.setTokenType(tokenType == null ? SINGLE_USE : tokenType)
|
||||
.setAllowedClientIds(ImmutableSet.copyOf(nullToEmpty(allowedClientIds)))
|
||||
.setAllowedTlds(ImmutableSet.copyOf(nullToEmpty(allowedTlds)))
|
||||
.setDiscountFraction(discountFraction);
|
||||
.setAllowedTlds(ImmutableSet.copyOf(nullToEmpty(allowedTlds)));
|
||||
Optional.ofNullable(discountFraction).ifPresent(token::setDiscountFraction);
|
||||
Optional.ofNullable(discountPremiums).ifPresent(token::setDiscountPremiums);
|
||||
Optional.ofNullable(discountYears).ifPresent(token::setDiscountYears);
|
||||
Optional.ofNullable(tokenStatusTransitions)
|
||||
.ifPresent(token::setTokenStatusTransitions);
|
||||
Optional.ofNullable(domainNames)
|
||||
|
||||
@@ -27,7 +27,7 @@ import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Command to show allocation tokens. */
|
||||
@Parameters(separators = " =", commandDescription = "Show allocation token(s)")
|
||||
@@ -55,11 +55,11 @@ final class GetAllocationTokenCommand implements CommandWithRemoteApi {
|
||||
if (loadedTokens.containsKey(token)) {
|
||||
AllocationToken loadedToken = loadedTokens.get(token);
|
||||
System.out.println(loadedToken.toString());
|
||||
if (loadedToken.getRedemptionHistoryEntry() == null) {
|
||||
if (!loadedToken.getRedemptionHistoryEntry().isPresent()) {
|
||||
System.out.printf("Token %s was not redeemed.\n", token);
|
||||
} else {
|
||||
DomainBase domain =
|
||||
domains.get(loadedToken.getRedemptionHistoryEntry().<DomainBase>getParent());
|
||||
domains.get(loadedToken.getRedemptionHistoryEntry().get().<DomainBase>getParent());
|
||||
if (domain == null) {
|
||||
System.out.printf("ERROR: Token %s was redeemed but domain can't be loaded.\n", token);
|
||||
} else {
|
||||
@@ -80,7 +80,8 @@ final class GetAllocationTokenCommand implements CommandWithRemoteApi {
|
||||
ImmutableList<Key<DomainBase>> domainKeys =
|
||||
tokens.stream()
|
||||
.map(AllocationToken::getRedemptionHistoryEntry)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.map(Key::<DomainBase>getParent)
|
||||
.collect(toImmutableList());
|
||||
ImmutableMap.Builder<Key<DomainBase>, DomainBase> domainsBuilder = new ImmutableMap.Builder<>();
|
||||
|
||||
@@ -14,15 +14,7 @@
|
||||
|
||||
package google.registry.tools;
|
||||
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.model.domain.DomainBase;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* A command to registry lock domain names.
|
||||
@@ -32,25 +24,6 @@ import org.joda.time.DateTime;
|
||||
@Parameters(separators = " =", commandDescription = "Registry lock a domain via EPP.")
|
||||
public class LockDomainCommand extends LockOrUnlockDomainCommand {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@Override
|
||||
protected boolean shouldApplyToDomain(String domain, DateTime now) {
|
||||
DomainBase domainBase =
|
||||
loadByForeignKey(DomainBase.class, domain, now)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalArgumentException(
|
||||
String.format("Domain '%s' does not exist or is deleted", domain)));
|
||||
ImmutableSet<StatusValue> statusesToAdd =
|
||||
Sets.difference(REGISTRY_LOCK_STATUSES, domainBase.getStatusValues()).immutableCopy();
|
||||
if (statusesToAdd.isEmpty()) {
|
||||
logger.atInfo().log("Domain '%s' is already locked and needs no updates.", domain);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createAndApplyRequest(String domain) {
|
||||
domainLockUtils.administrativelyApplyLock(domain, clientId, null, true);
|
||||
|
||||
@@ -15,22 +15,24 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Iterables.partition;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_DELETE_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED;
|
||||
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.CollectionUtils.findDuplicates;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Shared base class for commands to registry lock or unlock a domain via EPP. */
|
||||
public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand
|
||||
@@ -78,37 +80,37 @@ public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand
|
||||
@Override
|
||||
protected String execute() {
|
||||
ImmutableSet.Builder<String> successfulDomainsBuilder = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<String> skippedDomainsBuilder = new ImmutableSet.Builder<>();
|
||||
ImmutableSet.Builder<String> failedDomainsBuilder = new ImmutableSet.Builder<>();
|
||||
ImmutableMap.Builder<String, String> failedDomainsToReasons = new ImmutableMap.Builder<>();
|
||||
partition(getDomains(), BATCH_SIZE)
|
||||
.forEach(
|
||||
batch ->
|
||||
tm().transact(
|
||||
() -> {
|
||||
for (String domain : batch) {
|
||||
if (shouldApplyToDomain(domain, tm().getTransactionTime())) {
|
||||
try {
|
||||
createAndApplyRequest(domain);
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log(
|
||||
"Error when (un)locking domain %s.", domain);
|
||||
failedDomainsBuilder.add(domain);
|
||||
}
|
||||
successfulDomainsBuilder.add(domain);
|
||||
} else {
|
||||
skippedDomainsBuilder.add(domain);
|
||||
}
|
||||
}
|
||||
}));
|
||||
// we require that the jpaTm is the outer transaction in DomainLockUtils
|
||||
jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
tm().transact(
|
||||
() -> {
|
||||
for (String domain : batch) {
|
||||
try {
|
||||
createAndApplyRequest(domain);
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log(
|
||||
"Error when (un)locking domain %s.", domain);
|
||||
failedDomainsToReasons.put(domain, t.getMessage());
|
||||
continue;
|
||||
}
|
||||
successfulDomainsBuilder.add(domain);
|
||||
}
|
||||
})));
|
||||
ImmutableSet<String> successfulDomains = successfulDomainsBuilder.build();
|
||||
ImmutableSet<String> skippedDomains = skippedDomainsBuilder.build();
|
||||
ImmutableSet<String> failedDomains = failedDomainsBuilder.build();
|
||||
ImmutableSet<String> failedDomains =
|
||||
failedDomainsToReasons.build().entrySet().stream()
|
||||
.map(entry -> String.format("%s (%s)", entry.getKey(), entry.getValue()))
|
||||
.collect(toImmutableSet());
|
||||
return String.format(
|
||||
"Successfully locked/unlocked domains:\n%s\nSkipped domains:\n%s\nFailed domains:\n%s",
|
||||
successfulDomains, skippedDomains, failedDomains);
|
||||
"Successfully locked/unlocked domains:\n%s\nFailed domains:\n%s",
|
||||
successfulDomains, failedDomains);
|
||||
}
|
||||
|
||||
protected abstract boolean shouldApplyToDomain(String domain, DateTime now);
|
||||
|
||||
protected abstract void createAndApplyRequest(String domain);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.google.appengine.tools.remoteapi.RemoteApiOptions;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.beam.initsql.BeamJpaModule;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.model.ofy.ObjectifyService;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory;
|
||||
@@ -66,6 +65,13 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
|
||||
+ "If not set, credentials saved by running `nomulus login' will be used.")
|
||||
private String credentialJson = null;
|
||||
|
||||
@Parameter(
|
||||
names = {"--sql_access_info"},
|
||||
description =
|
||||
"Name of a file containing space-separated SQL access info used when deploying "
|
||||
+ "Beam pipelines")
|
||||
private String sqlAccessInfoFile = null;
|
||||
|
||||
// Do not make this final - compile-time constant inlining may interfere with JCommander.
|
||||
@ParametersDelegate
|
||||
private LoggingParameters loggingParams = new LoggingParameters();
|
||||
@@ -161,7 +167,7 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
|
||||
component =
|
||||
DaggerRegistryToolComponent.builder()
|
||||
.credentialFilePath(credentialJson)
|
||||
.beamJpaModule(new BeamJpaModule(credentialJson))
|
||||
.sqlAccessInfoFile(sqlAccessInfoFile)
|
||||
.build();
|
||||
|
||||
// JCommander stores sub-commands as nested JCommander objects containing a list of user objects
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user