mirror of
https://github.com/google/nomulus.git
synced 2025-07-21 18:26:12 +02:00
Refactor to be more in line with a standard Gradle project structure
This commit is contained in:
parent
98f87bcc03
commit
38cfc9f693
3141 changed files with 99 additions and 100 deletions
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2018 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.util;
|
||||
|
||||
/**
|
||||
* A wrapper for {@link com.google.appengine.api.modules.ModulesService} that provides a saner API.
|
||||
*/
|
||||
public interface AppEngineServiceUtils {
|
||||
|
||||
/**
|
||||
* Returns a host name to use for the given service.
|
||||
*
|
||||
* <p>Note that this host name will not include a version, so it will always be whatever the live
|
||||
* version is at the time that you hit the URL.
|
||||
*/
|
||||
String getServiceHostname(String service);
|
||||
|
||||
/**
|
||||
* Returns a host name to use for the given service and current version.
|
||||
*
|
||||
* <p>Note that this host name will include the current version now at time of URL generation,
|
||||
* which will not be the live version in the future.
|
||||
*/
|
||||
String getCurrentVersionHostname(String service);
|
||||
|
||||
/** Returns a host name to use for the given service and version. */
|
||||
String getVersionHostname(String service, String version);
|
||||
|
||||
/**
|
||||
* Converts a multi-level App Engine host name (not URL) to the -dot- single subdomain format.
|
||||
*
|
||||
* <p>This is needed because appspot.com only has a single wildcard SSL certificate, so the native
|
||||
* App Engine URLs of the form service.projectid.appspot.com or
|
||||
* version.service.projectid.appspot.com won't work over HTTPS when being fetched from outside of
|
||||
* GCP. The work-around is to change all of the "." subdomain markers to "-dot-". E.g.:
|
||||
*
|
||||
* <ul>
|
||||
* <li>tools.projectid.appspot.com --> tools-dot-projectid.appspot.com
|
||||
* <li>version.backend.projectid.appspot.com --> version-dot-backend-dot-projectid.appspot.com
|
||||
* </ul>
|
||||
*
|
||||
* @see <a
|
||||
* href="https://cloud.google.com/appengine/docs/standard/java/how-requests-are-routed">How
|
||||
* App Engine requests are routed</a>
|
||||
*/
|
||||
String convertToSingleSubdomain(String hostname);
|
||||
|
||||
/** Set the number of instances at runtime for a given service and version. */
|
||||
void setNumInstances(String service, String version, long numInstances);
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2018 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.appengine.api.modules.ModulesService;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** A wrapper for {@link ModulesService} that provides a saner API. */
|
||||
public class AppEngineServiceUtilsImpl implements AppEngineServiceUtils {
|
||||
|
||||
private static final Pattern APPSPOT_HOSTNAME_PATTERN =
|
||||
Pattern.compile("^(.*)\\.appspot\\.com$");
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final ModulesService modulesService;
|
||||
|
||||
@Inject
|
||||
public AppEngineServiceUtilsImpl(ModulesService modulesService) {
|
||||
this.modulesService = modulesService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceHostname(String service) {
|
||||
// This will be in the format "version.service.projectid.appspot.com"
|
||||
String hostnameWithVersion = modulesService.getVersionHostname(service, null);
|
||||
// Strip off the version and return just "service.projectid.appspot.com"
|
||||
return hostnameWithVersion.replaceFirst("^[^.]+\\.", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentVersionHostname(String service) {
|
||||
return modulesService.getVersionHostname(service, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersionHostname(String service, String version) {
|
||||
checkArgumentNotNull(version, "Must specify the version");
|
||||
return modulesService.getVersionHostname(service, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNumInstances(String service, String version, long numInstances) {
|
||||
checkArgumentNotNull(service, "Must specify the service");
|
||||
checkArgumentNotNull(version, "Must specify the version");
|
||||
checkArgument(numInstances > 0, "Number of instances must be greater than 0");
|
||||
modulesService.setNumInstances(service, version, numInstances);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToSingleSubdomain(String hostname) {
|
||||
Matcher matcher = APPSPOT_HOSTNAME_PATTERN.matcher(hostname);
|
||||
if (!matcher.matches()) {
|
||||
logger.atWarning().log("Skipping conversion because hostname can't be parsed: %s", hostname);
|
||||
return hostname;
|
||||
}
|
||||
return matcher.group(1).replace(".", "-dot-") + ".appspot.com";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.appengine.api.ThreadManager.currentRequestThreadFactory;
|
||||
|
||||
import com.google.common.util.concurrent.SimpleTimeLimiter;
|
||||
import com.google.common.util.concurrent.TimeLimiter;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A factory for {@link TimeLimiter} instances that use request threads, which carry the namespace
|
||||
* and live only as long as the request that spawned them.
|
||||
*
|
||||
* <p>It is safe to reuse instances of this class, but there is no benefit in doing so over creating
|
||||
* a fresh instance each time.
|
||||
*/
|
||||
public class AppEngineTimeLimiter {
|
||||
|
||||
/**
|
||||
* An {@code ExecutorService} that uses a new thread for every task.
|
||||
*
|
||||
* <p>We need to use fresh threads for each request so that we can use App Engine's request
|
||||
* threads. If we cached these threads in a thread pool (and if we were executing on a backend,
|
||||
* where there is no time limit on requests) the caching would cause the thread to keep the task
|
||||
* that opened it alive even after returning an http response, and would also cause the namespace
|
||||
* that the original thread was created in to leak out to later reuses of the thread.
|
||||
*
|
||||
* <p>Since there are no cached resources, this class doesn't have to support being shutdown.
|
||||
*/
|
||||
private static class NewRequestThreadExecutorService extends AbstractExecutorService {
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
currentRequestThreadFactory().newThread(command).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public static TimeLimiter create() {
|
||||
return SimpleTimeLimiter.create(new NewRequestThreadExecutorService());
|
||||
}
|
||||
}
|
27
util/src/main/java/google/registry/util/BUILD
Normal file
27
util/src/main/java/google/registry/util/BUILD
Normal file
|
@ -0,0 +1,27 @@
|
|||
package(
|
||||
default_visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
java_library(
|
||||
name = "util",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//third_party/jaxb",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_auto_value",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_dagger",
|
||||
"@com_google_errorprone_error_prone_annotations",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_re2j",
|
||||
"@com_ibm_icu_icu4j",
|
||||
"@javax_inject",
|
||||
"@joda_time",
|
||||
"@org_yaml_snakeyaml",
|
||||
],
|
||||
)
|
40
util/src/main/java/google/registry/util/BuildPathUtils.java
Normal file
40
util/src/main/java/google/registry/util/BuildPathUtils.java
Normal file
|
@ -0,0 +1,40 @@
|
|||
// 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.util;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/** Utilities methods related to build path. */
|
||||
public final class BuildPathUtils {
|
||||
// When we run the build from gradlew's directory, the current working directory would be
|
||||
// ${projectRoot}/gradle/${subproject}. So, the project root is the grand parent of it.
|
||||
private static final Path PROJECT_ROOT =
|
||||
Paths.get(System.getProperty("test.projectRoot", "../")).normalize();
|
||||
private static final Path RESOURCES_DIR =
|
||||
Paths.get(System.getProperty("test.resourcesDir", "build/resources/main")).normalize();
|
||||
|
||||
/** Returns the {@link Path} to the project root directory. */
|
||||
public static Path getProjectRoot() {
|
||||
return PROJECT_ROOT;
|
||||
}
|
||||
|
||||
/** Returns the {@link Path} to the resources directory. */
|
||||
public static Path getResourcesDir() {
|
||||
return RESOURCES_DIR;
|
||||
}
|
||||
|
||||
private BuildPathUtils() {}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** A log handler that captures logs. */
|
||||
public final class CapturingLogHandler extends Handler {
|
||||
|
||||
private final List<LogRecord> records = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void publish(@Nullable LogRecord record) {
|
||||
if (record != null) {
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
public Iterable<LogRecord> getRecords() {
|
||||
return Iterables.unmodifiableIterable(records);
|
||||
}
|
||||
}
|
479
util/src/main/java/google/registry/util/CidrAddressBlock.java
Normal file
479
util/src/main/java/google/registry/util/CidrAddressBlock.java
Normal file
|
@ -0,0 +1,479 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.collect.AbstractSequentialIterator;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import java.io.Serializable;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class representing an RFC 1519 CIDR IP address block.
|
||||
*
|
||||
* <p>When creating a CidrAddressBlock from an IP string literal
|
||||
* without a specified CIDR netmask (i.e. no trailing "/16" or "/64")
|
||||
* or an InetAddress with an accompanying integer netmask, then the
|
||||
* maximum length netmask for the address famiy of the specified
|
||||
* address is used (i.e. 32 for IPv4, 128 for IPv6). I.e. "1.2.3.4"
|
||||
* is automatically treated as "1.2.3.4/32" and, similarly,
|
||||
* "2001:db8::1" is automatically treated as "2001:db8::1/128".
|
||||
*
|
||||
*/
|
||||
// TODO(b/21870796): Migrate to Guava version when this is open-sourced.
|
||||
public class CidrAddressBlock implements Iterable<InetAddress>, Serializable {
|
||||
|
||||
/**
|
||||
* Wrapper class around a logger instance for {@link CidrAddressBlock}.
|
||||
*
|
||||
* <p>We don't want to have a static instance of {@link Logger} in {@link CidrAddressBlock},
|
||||
* because that can cause a race condition, since the logging subsystem might not yet be
|
||||
* initialized. With this wrapper, the {@link Logger} will be initialized on first use.
|
||||
*/
|
||||
static class CidrAddressBlockLogger {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
}
|
||||
|
||||
private final InetAddress ip;
|
||||
|
||||
/**
|
||||
* The number of block or mask bits needed to create the address block
|
||||
* (starting from the most-significant side of the address).
|
||||
*/
|
||||
private final int netmask;
|
||||
|
||||
/**
|
||||
* Attempts to parse the given String into a CIDR block.
|
||||
*
|
||||
* <p>If the string is an IP string literal without a specified
|
||||
* CIDR netmask (i.e. no trailing "/16" or "/64") then the maximum
|
||||
* length netmask for the address famiy of the specified address is
|
||||
* used (i.e. 32 for IPv4, 128 for IPv6).
|
||||
*
|
||||
* <p>The specified IP address portion must be properly truncated
|
||||
* (i.e. all the host bits must be zero) or the input is considered
|
||||
* malformed. For example, "1.2.3.0/24" is accepted but "1.2.3.4/24"
|
||||
* is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas
|
||||
* "2001:db8::1/32" is not.
|
||||
*
|
||||
* <p>If inputs might not be properly truncated but would be acceptable
|
||||
* to the application consider constructing a {@code CidrAddressBlock}
|
||||
* via {@code create()}.
|
||||
*
|
||||
* @param s a String of the form "217.68.0.0/16" or "2001:db8::/32".
|
||||
*
|
||||
* @throws IllegalArgumentException if s is malformed or does not
|
||||
* represent a valid CIDR block.
|
||||
*/
|
||||
public CidrAddressBlock(String s) {
|
||||
this(parseInetAddress(s), parseNetmask(s), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse the given String and int into a CIDR block.
|
||||
*
|
||||
* <p>The specified IP address portion must be properly truncated
|
||||
* (i.e. all the host bits must be zero) or the input is considered
|
||||
* malformed. For example, "1.2.3.0/24" is accepted but "1.2.3.4/24"
|
||||
* is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas
|
||||
* "2001:db8::1/32" is not.
|
||||
*
|
||||
* <p>An IP address without a netmask will automatically have the
|
||||
* maximum applicable netmask for its address family. I.e. "1.2.3.4"
|
||||
* is automatically treated as "1.2.3.4/32", and "2001:db8::1" is
|
||||
* automatically treated as "2001:db8::1/128".
|
||||
*
|
||||
* <p>If inputs might not be properly truncated but would be acceptable
|
||||
* to the application consider constructing a {@code CidrAddressBlock}
|
||||
* via {@code create()}.
|
||||
*
|
||||
* @param ip a String of the form "217.68.0.0" or "2001:db8::".
|
||||
* @param netmask an int between 0 and 32 (for IPv4) or 128 (for IPv6).
|
||||
* This is the number of bits, starting from the big end of the IP,
|
||||
* that will be used for network bits (as opposed to host bits)
|
||||
* in this CIDR block.
|
||||
*
|
||||
* @throws IllegalArgumentException if the params are malformed or do not
|
||||
* represent a valid CIDR block.
|
||||
*/
|
||||
public CidrAddressBlock(String ip, int netmask) {
|
||||
this(InetAddresses.forString(ip), checkNotNegative(netmask), false);
|
||||
}
|
||||
|
||||
public CidrAddressBlock(InetAddress ip) {
|
||||
this(ip, AUTO_NETMASK, false);
|
||||
}
|
||||
|
||||
public CidrAddressBlock(InetAddress ip, int netmask) {
|
||||
this(ip, checkNotNegative(netmask), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to construct a CIDR block from the IP address and netmask,
|
||||
* truncating the IP address as required.
|
||||
*
|
||||
* <p>The specified IP address portion need not be properly truncated
|
||||
* (i.e. all the host bits need not be zero); truncation will be silently
|
||||
* performed. For example, "1.2.3.4/24" is accepted and returns the
|
||||
* same {@code CidrAddressBlock} as "1.2.3.0/24". Similarly, for IPv6,
|
||||
* "2001:db8::1/32" is accepted and returns the same
|
||||
* {@code CidrAddressBlock} as "2001:db8::/32".
|
||||
*
|
||||
* @param ip {@link InetAddress}, possibly requiring truncation.
|
||||
* @param netmask an int between 0 and 32 (for IPv4) or 128 (for IPv6).
|
||||
* This is the number of bits, starting from the big end of the IP,
|
||||
* that will be used for network bits (as opposed to host bits)
|
||||
* when truncating the supplied {@link InetAddress}.
|
||||
*
|
||||
* @throws IllegalArgumentException if the params are malformed or do not
|
||||
* represent a valid CIDR block.
|
||||
* @throws NullPointerException if a parameter is null.
|
||||
*/
|
||||
public static CidrAddressBlock create(InetAddress ip, int netmask) {
|
||||
return new CidrAddressBlock(ip, checkNotNegative(netmask), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to construct a CIDR block from the IP address and netmask
|
||||
* expressed as a String, truncating the IP address as required.
|
||||
*
|
||||
* <p>The specified IP address portion need not be properly truncated
|
||||
* (i.e. all the host bits need not be zero); truncation will be silently
|
||||
* performed. For example, "1.2.3.4/24" is accepted and returns the
|
||||
* same {@code CidrAddressBlock} as "1.2.3.0/24". Similarly, for IPv6,
|
||||
* "2001:db8::1/32" is accepted and returns the same
|
||||
* {@code CidrAddressBlock} as "2001:db8::/32".
|
||||
*
|
||||
* @param s {@code String} representing either a single IP address or
|
||||
* a CIDR netblock, possibly requiring truncation.
|
||||
*
|
||||
* @throws IllegalArgumentException if the params are malformed or do not
|
||||
* represent a valid CIDR block.
|
||||
* @throws NullPointerException if a parameter is null.
|
||||
*/
|
||||
public static CidrAddressBlock create(String s) {
|
||||
return new CidrAddressBlock(parseInetAddress(s), parseNetmask(s), true);
|
||||
}
|
||||
|
||||
private static final int AUTO_NETMASK = -1;
|
||||
|
||||
/**
|
||||
* The universal constructor. All public constructors should lead here.
|
||||
*
|
||||
* @param ip {@link InetAddress}, possibly requiring truncation.
|
||||
* @param netmask the number of prefix bits to include in the netmask.
|
||||
* This is between 0 and 32 (for IPv4) or 128 (for IPv6).
|
||||
* The special value {@code AUTO_NETMASK} indicates that the CIDR block
|
||||
* should cover exactly one IP address.
|
||||
* @param truncate controls the behavior when an address has extra trailing
|
||||
* bits. If true, these bits are silently truncated, otherwise this
|
||||
* triggers an exception.
|
||||
*
|
||||
* @throws IllegalArgumentException if netmask is out of range, or ip has
|
||||
* unexpected trailing bits.
|
||||
* @throws NullPointerException if a parameter is null.
|
||||
*/
|
||||
private CidrAddressBlock(InetAddress ip, int netmask, boolean truncate) {
|
||||
// A single IP address is always truncated, by definition.
|
||||
if (netmask == AUTO_NETMASK) {
|
||||
this.ip = ip;
|
||||
this.netmask = ip.getAddress().length * 8;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the truncated form of this CIDR block.
|
||||
InetAddress truncatedIp = applyNetmask(ip, netmask);
|
||||
|
||||
// If we're not truncating silently, then check for trailing bits.
|
||||
if (!truncate && !truncatedIp.equals(ip)) {
|
||||
throw new IllegalArgumentException(
|
||||
"CIDR block: " + getCidrString(ip, netmask)
|
||||
+ " is not properly truncated, should have been: "
|
||||
+ getCidrString(truncatedIp, netmask));
|
||||
}
|
||||
|
||||
this.ip = truncatedIp;
|
||||
this.netmask = netmask;
|
||||
}
|
||||
|
||||
private static String getCidrString(InetAddress ip, int netmask) {
|
||||
return ip.getHostAddress() + "/" + netmask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse an {@link InetAddress} prefix from the given String.
|
||||
*
|
||||
* @param s a String of the form "217.68.0.0/16" or "2001:db8::/32".
|
||||
*
|
||||
* @throws IllegalArgumentException if s does not begin with an IP address.
|
||||
*/
|
||||
private static InetAddress parseInetAddress(String s) {
|
||||
int slash = s.indexOf('/');
|
||||
return InetAddresses.forString((slash < 0) ? s : s.substring(0, slash));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse a netmask from the given String.
|
||||
*
|
||||
* <p>If the string does not end with a "/xx" suffix, then return AUTO_NETMASK
|
||||
* and let the constructor handle it. Otherwise, we only verify that the
|
||||
* suffix is a well-formed nonnegative integer.
|
||||
*
|
||||
* @param s a String of the form "217.68.0.0/16" or "2001:db8::/32".
|
||||
*
|
||||
* @throws IllegalArgumentException if s is malformed or does not end with a
|
||||
* valid nonnegative integer.
|
||||
*/
|
||||
private static int parseNetmask(String s) {
|
||||
int slash = s.indexOf('/');
|
||||
if (slash < 0) {
|
||||
return AUTO_NETMASK;
|
||||
}
|
||||
try {
|
||||
return checkNotNegative(Integer.parseInt(s.substring(slash + 1)));
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new IllegalArgumentException("Invalid netmask: " + s.substring(slash + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static int checkNotNegative(int netmask) {
|
||||
checkArgument(netmask >= 0, "CIDR netmask '%s' must not be negative.", netmask);
|
||||
return netmask;
|
||||
}
|
||||
|
||||
private static InetAddress applyNetmask(InetAddress ip, int netmask) {
|
||||
byte[] bytes = ip.getAddress();
|
||||
checkArgument(
|
||||
(netmask >= 0) && (netmask <= (bytes.length * 8)),
|
||||
"CIDR netmask '%s' is out of range: 0 <= netmask <= %s.",
|
||||
netmask, (bytes.length * 8));
|
||||
|
||||
// The byte in which the CIDR boundary falls.
|
||||
int cidrByte = (netmask == 0) ? 0 : ((netmask - 1) / 8);
|
||||
// The number of mask bits within this byte.
|
||||
int numBits = netmask - (cidrByte * 8);
|
||||
// The bitmask for this byte.
|
||||
int bitMask = (-1 << (8 - numBits));
|
||||
|
||||
// Truncate the byte in which the CIDR boundary falls.
|
||||
bytes[cidrByte] = (byte) (bytes[cidrByte] & bitMask);
|
||||
|
||||
// All bytes following the cidrByte get zeroed.
|
||||
for (int i = cidrByte + 1; i < bytes.length; ++i) {
|
||||
bytes[i] = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return InetAddress.getByAddress(bytes);
|
||||
} catch (UnknownHostException uhe) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Error creating InetAddress from byte array '%s'.",
|
||||
Arrays.toString(bytes)),
|
||||
uhe);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the standard {@code String} representation of the IP portion
|
||||
* of this CIDR block (a.b.c.d, or a:b:c::d)
|
||||
*
|
||||
* <p>NOTE: This is not reliable for comparison operations. It is
|
||||
* more reliable to normalize strings into {@link InetAddress}s and
|
||||
* then compare.
|
||||
*
|
||||
* <p>Consider:
|
||||
* <ul>
|
||||
* <li>{@code "10.11.12.0"} is equivalent to {@code "10.11.12.000"}
|
||||
* <li>{@code "2001:db8::"} is equivalent to
|
||||
* {@code "2001:0DB8:0000:0000:0000:0000:0000:0000"}
|
||||
* </ul>
|
||||
*/
|
||||
public String getIp() {
|
||||
return ip.getHostAddress();
|
||||
}
|
||||
|
||||
public InetAddress getInetAddress() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of leading bits (prefix size) of the routing prefix.
|
||||
*/
|
||||
public int getNetmask() {
|
||||
return netmask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the supplied {@link InetAddress} is within
|
||||
* this {@code CidrAddressBlock}, {@code false} otherwise.
|
||||
*
|
||||
* <p>This can be used to test if the argument falls within a well-known
|
||||
* network range, a la GoogleIp's isGoogleIp(), isChinaIp(), et alia.
|
||||
*
|
||||
* @param ipAddr {@link InetAddress} to evaluate.
|
||||
* @return {@code true} if {@code ipAddr} is logically within this block,
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
public boolean contains(@Nullable InetAddress ipAddr) {
|
||||
if (ipAddr == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// IPv4 CIDR netblocks can never contain IPv6 addresses, and vice versa.
|
||||
// Calling getClass() is safe because the Inet4Address and Inet6Address
|
||||
// classes are final.
|
||||
if (ipAddr.getClass() != ip.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return ip.equals(applyNetmask(ipAddr, netmask));
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
// Something has gone very wrong. This CidrAddressBlock should
|
||||
// not have been created with an invalid netmask and a valid
|
||||
// netmask should have been successfully applied to "ipAddr" as long
|
||||
// as it represents an address of the same family as "this.ip".
|
||||
CidrAddressBlockLogger.logger.atWarning().withCause(e).log("Error while applying netmask.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the supplied {@code CidrAddressBlock} is within
|
||||
* this {@code CidrAddressBlock}, {@code false} otherwise.
|
||||
*
|
||||
* <p>This can be used to test if the argument falls within a well-known
|
||||
* network range, a la GoogleIp's isGoogleIp(), isChinaIp(), et alia.
|
||||
*
|
||||
* @param cidr {@code CidrAddressBlock} to evaluate.
|
||||
* @return {@code true} if {@code cidr} is logically within this block,
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
public boolean contains(@Nullable CidrAddressBlock cidr) {
|
||||
if (cidr == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cidr.netmask < netmask) {
|
||||
// No block can contain a network larger than it
|
||||
// (in CIDR larger blocks have smaller netmasks).
|
||||
return false;
|
||||
}
|
||||
|
||||
return contains(cidr.getInetAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the supplied {@code String} is within
|
||||
* this {@code CidrAddressBlock}, {@code false} otherwise.
|
||||
*
|
||||
* <p>This can be used to test if the argument falls within a well-known
|
||||
* network range, a la GoogleIp's isGoogleIp(), isChinaIp(), et alia.
|
||||
*
|
||||
* @param s {@code String} to evaluate.
|
||||
* @return {@code true} if {@code s} is logically within this block,
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
public boolean contains(@Nullable String s) {
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return contains(create(s));
|
||||
} catch (IllegalArgumentException iae) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the address that is contained in this {@code CidrAddressBlock}
|
||||
* with the most bits set.
|
||||
*
|
||||
* <p>This can be used to calculate the upper bound address of the address
|
||||
* range for this {@code CidrAddressBlock}.
|
||||
*/
|
||||
public InetAddress getAllOnesAddress() {
|
||||
byte[] bytes = ip.getAddress();
|
||||
|
||||
// The byte in which the CIDR boundary falls.
|
||||
int cidrByte = (netmask == 0) ? 0 : ((netmask - 1) / 8);
|
||||
// The number of mask bits within this byte.
|
||||
int numBits = netmask - (cidrByte * 8);
|
||||
// The bitmask for this byte.
|
||||
int bitMask = ~(-1 << (8 - numBits));
|
||||
|
||||
// Set all non-prefix bits where the CIDR boundary falls.
|
||||
bytes[cidrByte] = (byte) (bytes[cidrByte] | bitMask);
|
||||
|
||||
// All bytes following the cidrByte get set to all ones.
|
||||
for (int i = cidrByte + 1; i < bytes.length; ++i) {
|
||||
bytes[i] = (byte) 0xff;
|
||||
}
|
||||
|
||||
try {
|
||||
return InetAddress.getByAddress(bytes);
|
||||
} catch (UnknownHostException uhe) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Error creating InetAddress from byte array '%s'.",
|
||||
Arrays.toString(bytes)),
|
||||
uhe);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<InetAddress> iterator() {
|
||||
return new AbstractSequentialIterator<InetAddress>(ip) {
|
||||
@Override
|
||||
protected InetAddress computeNext(InetAddress previous) {
|
||||
if (InetAddresses.isMaximum(previous)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InetAddress next = InetAddresses.increment(previous);
|
||||
return contains(next) ? next : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// InetAddresses.coerceToInteger() will be deprecated in the future.
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return InetAddresses.coerceToInteger(ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (!(o instanceof CidrAddressBlock)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CidrAddressBlock cidr = (CidrAddressBlock) o;
|
||||
return ip.equals(cidr.ip) && (netmask == cidr.netmask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getCidrString(ip, netmask);
|
||||
}
|
||||
}
|
34
util/src/main/java/google/registry/util/Clock.java
Normal file
34
util/src/main/java/google/registry/util/Clock.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
// 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.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* A clock that tells the current time in milliseconds or nanoseconds.
|
||||
*
|
||||
* <p>Clocks are technically serializable because they are either a stateless wrapper around the
|
||||
* system clock, or for testing, are just a wrapper around a DateTime. This means that if you
|
||||
* serialize a clock and deserialize it elsewhere, you won't necessarily get the same time or time
|
||||
* zone -- what you will get is a functioning clock.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public interface Clock extends Serializable {
|
||||
|
||||
/** Returns current time in UTC timezone. */
|
||||
DateTime nowUtc();
|
||||
}
|
151
util/src/main/java/google/registry/util/CollectionUtils.java
Normal file
151
util/src/main/java/google/registry/util/CollectionUtils.java
Normal file
|
@ -0,0 +1,151 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.collect.Iterables.isEmpty;
|
||||
import static com.google.common.collect.Iterables.partition;
|
||||
|
||||
import com.google.common.collect.HashMultiset;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Multisets;
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Utility methods related to collections. */
|
||||
public class CollectionUtils {
|
||||
|
||||
/** Checks if an iterable is null or empty. */
|
||||
public static boolean isNullOrEmpty(@Nullable Iterable<?> potentiallyNull) {
|
||||
return potentiallyNull == null || isEmpty(potentiallyNull);
|
||||
}
|
||||
|
||||
/** Checks if a map is null or empty. */
|
||||
public static boolean isNullOrEmpty(@Nullable Map<?, ?> potentiallyNull) {
|
||||
return potentiallyNull == null || potentiallyNull.isEmpty();
|
||||
}
|
||||
|
||||
/** Turns a null set into an empty set. JAXB leaves lots of null sets lying around. */
|
||||
public static <T> Set<T> nullToEmpty(@Nullable Set<T> potentiallyNull) {
|
||||
return firstNonNull(potentiallyNull, ImmutableSet.of());
|
||||
}
|
||||
|
||||
/** Turns a null list into an empty list. */
|
||||
public static <T> List<T> nullToEmpty(@Nullable List<T> potentiallyNull) {
|
||||
return firstNonNull(potentiallyNull, ImmutableList.of());
|
||||
}
|
||||
|
||||
/** Turns a null map into an empty map. */
|
||||
public static <T, U> Map<T, U> nullToEmpty(@Nullable Map<T, U> potentiallyNull) {
|
||||
return firstNonNull(potentiallyNull, ImmutableMap.of());
|
||||
}
|
||||
|
||||
/** Turns a null multimap into an empty multimap. */
|
||||
public static <T, U> Multimap<T, U> nullToEmpty(@Nullable Multimap<T, U> potentiallyNull) {
|
||||
return firstNonNull(potentiallyNull, ImmutableMultimap.of());
|
||||
}
|
||||
|
||||
/** Turns a null sorted map into an empty sorted map.. */
|
||||
public static <T, U> SortedMap<T, U> nullToEmpty(@Nullable SortedMap<T, U> potentiallyNull) {
|
||||
return firstNonNull(potentiallyNull, ImmutableSortedMap.of());
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link Set}. */
|
||||
public static <V> ImmutableSet<V> nullSafeImmutableCopy(Set<V> data) {
|
||||
return data == null ? null : ImmutableSet.copyOf(data);
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link List}. */
|
||||
public static <V> ImmutableList<V> nullSafeImmutableCopy(List<V> data) {
|
||||
return data == null ? null : ImmutableList.copyOf(data);
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link Set}. */
|
||||
public static <V> ImmutableSet<V> nullToEmptyImmutableCopy(Set<V> data) {
|
||||
return data == null ? ImmutableSet.of() : ImmutableSet.copyOf(data);
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link Set}. */
|
||||
public static <V extends Comparable<V>>
|
||||
ImmutableSortedSet<V> nullToEmptyImmutableSortedCopy(Set<V> data) {
|
||||
return data == null ? ImmutableSortedSet.of() : ImmutableSortedSet.copyOf(data);
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link SortedMap}. */
|
||||
public static <K, V> ImmutableSortedMap<K, V> nullToEmptyImmutableCopy(SortedMap<K, V> data) {
|
||||
return data == null ? ImmutableSortedMap.of() : ImmutableSortedMap.copyOfSorted(data);
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link List}. */
|
||||
public static <V> ImmutableList<V> nullToEmptyImmutableCopy(List<V> data) {
|
||||
return data == null ? ImmutableList.of() : ImmutableList.copyOf(data);
|
||||
}
|
||||
|
||||
/** Defensive copy helper for {@link Map}. */
|
||||
public static <K, V> ImmutableMap<K, V> nullToEmptyImmutableCopy(Map<K, V> data) {
|
||||
return data == null ? ImmutableMap.of() : ImmutableMap.copyOf(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an empty collection into a null collection.
|
||||
*
|
||||
* <p>This is unwise in the general case (nulls are bad; empties are good) but occasionally needed
|
||||
* to cause JAXB not to emit a field, or to avoid saving something to Datastore. The method name
|
||||
* includes "force" to indicate that you should think twice before using it.
|
||||
*/
|
||||
@Nullable
|
||||
public static <T, C extends Collection<T>> C forceEmptyToNull(@Nullable C potentiallyEmpty) {
|
||||
return potentiallyEmpty == null || potentiallyEmpty.isEmpty() ? null : potentiallyEmpty;
|
||||
}
|
||||
|
||||
/** Copy an {@link ImmutableSet} and add members. */
|
||||
@SafeVarargs
|
||||
public static <T> ImmutableSet<T> union(Set<T> set, T... newMembers) {
|
||||
return Sets.union(set, ImmutableSet.copyOf(newMembers)).immutableCopy();
|
||||
}
|
||||
|
||||
/** Copy an {@link ImmutableSet} and remove members. */
|
||||
@SafeVarargs
|
||||
public static <T> ImmutableSet<T> difference(Set<T> set, T... toRemove) {
|
||||
return Sets.difference(set, ImmutableSet.copyOf(toRemove)).immutableCopy();
|
||||
}
|
||||
|
||||
/** Returns any duplicates in an iterable. */
|
||||
public static <T> Set<T> findDuplicates(Iterable<T> iterable) {
|
||||
return Multisets.difference(
|
||||
HashMultiset.create(iterable),
|
||||
HashMultiset.create(ImmutableSet.copyOf(iterable))).elementSet();
|
||||
}
|
||||
|
||||
/** Partitions a Map into a Collection of Maps, each of max size n. */
|
||||
public static <K, V> ImmutableList<ImmutableMap<K, V>> partitionMap(Map<K, V> map, int size) {
|
||||
ImmutableList.Builder<ImmutableMap<K, V>> shards = new ImmutableList.Builder<>();
|
||||
for (Iterable<Map.Entry<K, V>> entriesShard : partition(map.entrySet(), size)) {
|
||||
shards.add(ImmutableMap.copyOf(entriesShard));
|
||||
}
|
||||
return shards.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.common.reflect.Reflection;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Abstract InvocationHandler comparing two implementations of some interface.
|
||||
*
|
||||
* <p>Given an interface, and two instances of that interface (the "original" instance we know
|
||||
* works, and a "second" instance we wish to test), creates an InvocationHandler that acts like an
|
||||
* exact proxy to the "original" instance.
|
||||
*
|
||||
* <p>In addition, it will log any differences in return values or thrown exception between the
|
||||
* "original" and "second" instances.
|
||||
*
|
||||
* <p>This can be used to create an exact proxy to the original instance that can be placed in any
|
||||
* code, while live testing the second instance.
|
||||
*/
|
||||
public abstract class ComparingInvocationHandler<T> implements InvocationHandler {
|
||||
|
||||
private final T actualImplementation;
|
||||
private final T secondImplementation;
|
||||
private final Class<T> interfaceClass;
|
||||
|
||||
/**
|
||||
* Creates a new InvocationHandler for the given interface.
|
||||
*
|
||||
* @param interfaceClass the interface we want to create.
|
||||
* @param actualImplementation the resulting proxy will be an exact proxy to this object
|
||||
* @param secondImplementation Only used to log difference compared to actualImplementation.
|
||||
* Otherwise has no effect on the resulting proxy's behavior.
|
||||
*/
|
||||
public ComparingInvocationHandler(
|
||||
Class<T> interfaceClass, T actualImplementation, T secondImplementation) {
|
||||
this.actualImplementation = actualImplementation;
|
||||
this.secondImplementation = secondImplementation;
|
||||
this.interfaceClass = interfaceClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proxy to the actualImplementation.
|
||||
*
|
||||
* <p>The return value is a drop-in replacement to the actualImplementation, but will log any
|
||||
* difference with the secondImplementation during normal execution.
|
||||
*/
|
||||
public final T makeProxy() {
|
||||
return Reflection.newProxy(interfaceClass, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there was a difference between the implementations.
|
||||
*
|
||||
* @param method the method where the difference was found
|
||||
* @param message human readable description of the difference found
|
||||
*/
|
||||
protected abstract void log(Method method, String message);
|
||||
|
||||
/**
|
||||
* Implements toString for specific types.
|
||||
*
|
||||
* <p>By default objects are logged using their .toString. If .toString isn't implemented for
|
||||
* some relevant classes (or if we want to use a different version), override this method with
|
||||
* the desired implementation.
|
||||
*
|
||||
* @param method the method whose return value is given
|
||||
* @param object the object returned by a call to method
|
||||
*/
|
||||
protected String stringifyResult(
|
||||
@SuppressWarnings("unused") Method method,
|
||||
@Nullable Object object) {
|
||||
return String.valueOf(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the method results are as similar as we expect.
|
||||
*
|
||||
* <p>By default objects are compared using their .equals. If .equals isn't implemented for some
|
||||
* relevant classes (or if we want change what is considered "not equal"), override this method
|
||||
* with the desired implementation.
|
||||
*
|
||||
* @param method the method whose return value is given
|
||||
* @param actual the object returned by a call to method for the "actual" implementation
|
||||
* @param second the object returned by a call to method for the "second" implementation
|
||||
*/
|
||||
protected boolean compareResults(
|
||||
@SuppressWarnings("unused") Method method,
|
||||
@Nullable Object actual,
|
||||
@Nullable Object second) {
|
||||
return Objects.equals(actual, second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the thrown exceptions are as similar as we expect.
|
||||
*
|
||||
* <p>By default this returns 'true' for any input: all we check by default is that both
|
||||
* implementations threw something. Override if you need to actually compare both throwables.
|
||||
*
|
||||
* @param method the method whose return value is given
|
||||
* @param actual the exception thrown by a call to method for the "actual" implementation
|
||||
* @param second the exception thrown by a call to method for the "second" implementation
|
||||
*/
|
||||
protected boolean compareThrown(
|
||||
@SuppressWarnings("unused") Method method,
|
||||
Throwable actual,
|
||||
Throwable second) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements toString for thrown exceptions.
|
||||
*
|
||||
* <p>By default exceptions are logged using their .toString. If more data is needed (part of
|
||||
* stack trace for example), override this method with the desired implementation.
|
||||
*
|
||||
* @param method the method whose return value is given
|
||||
* @param throwable the exception thrown by a call to method
|
||||
*/
|
||||
protected String stringifyThrown(
|
||||
@SuppressWarnings("unused") Method method,
|
||||
Throwable throwable) {
|
||||
return throwable.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
Object actualResult = null;
|
||||
Throwable actualException = null;
|
||||
try {
|
||||
actualResult = method.invoke(actualImplementation, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
actualException = e.getCause();
|
||||
}
|
||||
|
||||
Object secondResult = null;
|
||||
Throwable secondException = null;
|
||||
try {
|
||||
secondResult = method.invoke(secondImplementation, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
secondException = e.getCause();
|
||||
}
|
||||
|
||||
// First compare the two implementations' result, and log any differences:
|
||||
if (actualException != null && secondException != null) {
|
||||
if (!compareThrown(method, actualException, secondException)) {
|
||||
log(
|
||||
method,
|
||||
String.format(
|
||||
"Both implementations threw, but got different exceptions! '%s' vs '%s'",
|
||||
stringifyThrown(method, actualException),
|
||||
stringifyThrown(method, secondException)));
|
||||
}
|
||||
} else if (actualException != null) {
|
||||
log(
|
||||
method,
|
||||
String.format(
|
||||
"Only actual implementation threw exception: %s",
|
||||
stringifyThrown(method, actualException)));
|
||||
} else if (secondException != null) {
|
||||
log(
|
||||
method,
|
||||
String.format(
|
||||
"Only second implementation threw exception: %s",
|
||||
stringifyThrown(method, secondException)));
|
||||
} else {
|
||||
// Neither threw exceptions - we compare the results
|
||||
if (!compareResults(method, actualResult, secondResult)) {
|
||||
log(
|
||||
method,
|
||||
String.format(
|
||||
"Got different results! '%s' vs '%s'",
|
||||
stringifyResult(method, actualResult),
|
||||
stringifyResult(method, secondResult)));
|
||||
}
|
||||
}
|
||||
|
||||
// Now reproduce the actual implementation's behavior:
|
||||
if (actualException != null) {
|
||||
throw actualException;
|
||||
}
|
||||
return actualResult;
|
||||
}
|
||||
}
|
100
util/src/main/java/google/registry/util/Concurrent.java
Normal file
100
util/src/main/java/google/registry/util/Concurrent.java
Normal file
|
@ -0,0 +1,100 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.appengine.api.ThreadManager.currentRequestThreadFactory;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static java.util.concurrent.Executors.newFixedThreadPool;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.function.Function;
|
||||
|
||||
/** Utilities for multithreaded operations in App Engine requests. */
|
||||
public final class Concurrent {
|
||||
|
||||
/** Maximum number of threads per pool. The actual GAE per-request limit is 50. */
|
||||
private static final int MAX_THREADS = 10;
|
||||
|
||||
/**
|
||||
* Runs transform with the default number of threads.
|
||||
*
|
||||
* @see #transform(Collection, int, Function)
|
||||
*/
|
||||
public static <A, B> ImmutableList<B> transform(Collection<A> items, final Function<A, B> funk) {
|
||||
return transform(items, MAX_THREADS, funk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes {@code items} in parallel using {@code funk}, with the specified number of threads.
|
||||
*
|
||||
* <p>If the maxThreadCount or the number of items is less than 2, will use a non-concurrent
|
||||
* transform.
|
||||
*
|
||||
* <p><b>Note:</b> Spawned threads will inherit the same namespace.
|
||||
*
|
||||
* @throws UncheckedExecutionException to wrap the exception thrown by {@code funk}. This will
|
||||
* only contain the exception information for the first exception thrown.
|
||||
* @return transformed {@code items} in the same order.
|
||||
*/
|
||||
public static <A, B> ImmutableList<B> transform(
|
||||
Collection<A> items,
|
||||
int maxThreadCount,
|
||||
final Function<A, B> funk) {
|
||||
checkNotNull(funk);
|
||||
checkNotNull(items);
|
||||
int threadCount = max(1, min(items.size(), maxThreadCount));
|
||||
ThreadFactory threadFactory = threadCount > 1 ? currentRequestThreadFactory() : null;
|
||||
if (threadFactory == null) {
|
||||
// Fall back to non-concurrent transform if we only want 1 thread, or if we can't get an App
|
||||
// Engine thread factory (most likely caused by hitting this code from a command-line tool).
|
||||
// Default Java system threads are not compatible with code that needs to interact with App
|
||||
// Engine (such as Objectify), which we often have in funk when calling
|
||||
// Concurrent.transform(). For more info see: http://stackoverflow.com/questions/15976406
|
||||
return items.stream().map(funk).collect(toImmutableList());
|
||||
}
|
||||
ExecutorService executor = newFixedThreadPool(threadCount, threadFactory);
|
||||
try {
|
||||
List<Future<B>> futures = new ArrayList<>();
|
||||
for (final A item : items) {
|
||||
futures.add(executor.submit(() -> funk.apply(item)));
|
||||
}
|
||||
ImmutableList.Builder<B> results = new ImmutableList.Builder<>();
|
||||
for (Future<B> future : futures) {
|
||||
try {
|
||||
results.add(Uninterruptibles.getUninterruptibly(future));
|
||||
} catch (ExecutionException e) {
|
||||
throw new UncheckedExecutionException(e.getCause());
|
||||
}
|
||||
}
|
||||
return results.build();
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private Concurrent() {}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.appengine.api.datastore.Key;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Utility methods for working with the App Engine Datastore service. */
|
||||
public class DatastoreServiceUtils {
|
||||
|
||||
/** Returns the name or id of a key, which may be a string or a long. */
|
||||
public static Object getNameOrId(Key key) {
|
||||
return Optional.<Object>ofNullable(key.getName()).orElse(key.getId());
|
||||
}
|
||||
}
|
89
util/src/main/java/google/registry/util/DateTimeUtils.java
Normal file
89
util/src/main/java/google/registry/util/DateTimeUtils.java
Normal file
|
@ -0,0 +1,89 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Ordering;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
/** Utilities methods and constants related to Joda {@link DateTime} objects. */
|
||||
public class DateTimeUtils {
|
||||
|
||||
/** The start of the epoch, in a convenient constant. */
|
||||
public static final DateTime START_OF_TIME = new DateTime(0, DateTimeZone.UTC);
|
||||
|
||||
/**
|
||||
* A date in the far future that we can treat as infinity.
|
||||
*
|
||||
* <p>This value is (2^63-1)/1000 rounded down. AppEngine stores dates as 64 bit microseconds, but
|
||||
* Java uses milliseconds, so this is the largest representable date that will survive a
|
||||
* round-trip through Datastore.
|
||||
*/
|
||||
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);
|
||||
|
||||
/** Returns the earliest of a number of given {@link DateTime} instances. */
|
||||
public static DateTime earliestOf(DateTime first, DateTime... rest) {
|
||||
return earliestOf(Lists.asList(first, rest));
|
||||
}
|
||||
|
||||
/** Returns the earliest element in a {@link DateTime} iterable. */
|
||||
public static DateTime earliestOf(Iterable<DateTime> dates) {
|
||||
checkArgument(!Iterables.isEmpty(dates));
|
||||
return Ordering.<DateTime>natural().min(dates);
|
||||
}
|
||||
|
||||
/** Returns the latest of a number of given {@link DateTime} instances. */
|
||||
public static DateTime latestOf(DateTime first, DateTime... rest) {
|
||||
return latestOf(Lists.asList(first, rest));
|
||||
}
|
||||
|
||||
/** Returns the latest element in a {@link DateTime} iterable. */
|
||||
public static DateTime latestOf(Iterable<DateTime> dates) {
|
||||
checkArgument(!Iterables.isEmpty(dates));
|
||||
return Ordering.<DateTime>natural().max(dates);
|
||||
}
|
||||
|
||||
/** Returns whether the first {@link DateTime} is equal to or earlier than the second. */
|
||||
public static boolean isBeforeOrAt(DateTime timeToCheck, DateTime timeToCompareTo) {
|
||||
return !timeToCheck.isAfter(timeToCompareTo);
|
||||
}
|
||||
|
||||
/** Returns whether the first {@link DateTime} is equal to or later than the second. */
|
||||
public static boolean isAtOrAfter(DateTime timeToCheck, DateTime timeToCompareTo) {
|
||||
return !timeToCheck.isBefore(timeToCompareTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds years to a date, in the {@code Duration} sense of semantic years. Use this instead of
|
||||
* {@link DateTime#plusYears} to ensure that we never end up on February 29.
|
||||
*/
|
||||
public static DateTime leapSafeAddYears(DateTime now, int years) {
|
||||
checkArgument(years >= 0);
|
||||
return years == 0 ? now : now.plusYears(1).plusYears(years - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts years from a date, in the {@code Duration} sense of semantic years. Use this instead
|
||||
* of {@link DateTime#minusYears} to ensure that we never end up on February 29.
|
||||
*/
|
||||
public static DateTime leapSafeSubtractYears(DateTime now, int years) {
|
||||
checkArgument(years >= 0);
|
||||
return years == 0 ? now : now.minusYears(1).minusYears(years - 1);
|
||||
}
|
||||
}
|
181
util/src/main/java/google/registry/util/DiffUtils.java
Normal file
181
util/src/main/java/google/registry/util/DiffUtils.java
Normal file
|
@ -0,0 +1,181 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Helper class for diff utilities. */
|
||||
public final class DiffUtils {
|
||||
|
||||
/**
|
||||
* A helper class to store the two sides of a diff. If both sides are Sets then they will be
|
||||
* diffed, otherwise the two objects are toStringed in Collection format "[a, b]".
|
||||
*/
|
||||
private static class DiffPair {
|
||||
@Nullable
|
||||
final Object a;
|
||||
|
||||
@Nullable
|
||||
final Object b;
|
||||
|
||||
DiffPair(@Nullable Object a, @Nullable Object b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Note that we use newArrayList here instead of ImmutableList because a and b can be null.
|
||||
return String.format("%s -> %s", a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/** Pretty-prints a deep diff between two maps that represent Datastore entities. */
|
||||
public static String prettyPrintEntityDeepDiff(Map<?, ?> a, Map<?, ?> b) {
|
||||
return prettyPrintDiffedMap(deepDiff(a, b, true), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-prints a deep diff between two maps that represent XML documents. Path is prefixed to
|
||||
* each output line of the diff.
|
||||
*/
|
||||
public static String prettyPrintXmlDeepDiff(Map<?, ?> a, Map<?, ?> b, @Nullable String path) {
|
||||
return prettyPrintDiffedMap(deepDiff(a, b, false), path);
|
||||
}
|
||||
|
||||
/** Compare two maps and return a map containing, at each key where they differed, both values. */
|
||||
public static ImmutableMap<?, ?> deepDiff(
|
||||
Map<?, ?> a, Map<?, ?> b, boolean ignoreNullToCollection) {
|
||||
ImmutableMap.Builder<Object, Object> diff = new ImmutableMap.Builder<>();
|
||||
for (Object key : Sets.union(a.keySet(), b.keySet())) {
|
||||
Object aValue = a.get(key);
|
||||
Object bValue = b.get(key);
|
||||
if (Objects.equals(aValue, bValue)) {
|
||||
// The objects are equal, so print nothing.
|
||||
} else if (ignoreNullToCollection
|
||||
&& aValue == null
|
||||
&& bValue instanceof Collection
|
||||
&& ((Collection<?>) bValue).isEmpty()) {
|
||||
// Ignore a mismatch between Objectify's use of null to store empty collections and our
|
||||
// code's builder methods, which yield empty collections for the same fields. This
|
||||
// prevents useless lines of the form "[null, []]" from appearing in diffs.
|
||||
} else {
|
||||
// The objects aren't equal, so output a diff.
|
||||
if (aValue instanceof String && bValue instanceof String
|
||||
&& a.toString().contains("\n") && b.toString().contains("\n")) {
|
||||
aValue = stringToMap((String) aValue);
|
||||
bValue = stringToMap((String) bValue);
|
||||
} else if (aValue instanceof Set && bValue instanceof Set) {
|
||||
// Leave Sets alone; prettyPrintDiffedMap has special handling for Sets.
|
||||
} else if (aValue instanceof Iterable && bValue instanceof Iterable) {
|
||||
aValue = iterableToSortedMap((Iterable<?>) aValue);
|
||||
bValue = iterableToSortedMap((Iterable<?>) bValue);
|
||||
}
|
||||
diff.put(key, (aValue instanceof Map && bValue instanceof Map)
|
||||
? deepDiff((Map<?, ?>) aValue, (Map<?, ?>) bValue, ignoreNullToCollection)
|
||||
: new DiffPair(aValue, bValue));
|
||||
}
|
||||
}
|
||||
return diff.build();
|
||||
}
|
||||
|
||||
private static Map<Integer, ?> iterableToSortedMap(Iterable<?> iterable) {
|
||||
// We use a sorted map here so that the iteration across the keySet is consistent.
|
||||
ImmutableSortedMap.Builder<Integer, Object> builder =
|
||||
new ImmutableSortedMap.Builder<>(Ordering.natural());
|
||||
int i = 0;
|
||||
for (Object item : Iterables.filter(iterable, Objects::nonNull)) {
|
||||
builder.put(i++, item);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static Map<String, ?> stringToMap(String string) {
|
||||
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
|
||||
int i = 0;
|
||||
for (String item : Splitter.on('\n').split(string)) {
|
||||
builder.put("Line " + i++, item);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Recursively pretty prints the contents of a diffed map generated by {@link #deepDiff}. */
|
||||
public static String prettyPrintDiffedMap(Map<?, ?> map, @Nullable String path) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String newPath = (path == null ? "" : path + ".") + entry.getKey();
|
||||
String output;
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map) {
|
||||
output = prettyPrintDiffedMap((Map<?, ?>) entry.getValue(), newPath);
|
||||
} else if (value instanceof DiffPair
|
||||
&& ((DiffPair) value).a instanceof Set
|
||||
&& ((DiffPair) value).b instanceof Set) {
|
||||
DiffPair pair = ((DiffPair) value);
|
||||
String prettyLineDiff = prettyPrintSetDiff((Set<?>) pair.a, (Set<?>) pair.b) + "\n";
|
||||
output = newPath + (prettyLineDiff.startsWith("\n") ? ":" : ": ") + prettyLineDiff;
|
||||
} else {
|
||||
output = newPath + ": " + value + "\n";
|
||||
}
|
||||
builder.append(output);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string displaying the differences between the old values in a set and the new ones.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String prettyPrintSetDiff(Set<?> a, Set<?> b) {
|
||||
Set<?> removed = Sets.difference(a, b);
|
||||
Set<?> added = Sets.difference(b, a);
|
||||
if (removed.isEmpty() && added.isEmpty()) {
|
||||
return "NO DIFFERENCES";
|
||||
}
|
||||
return Joiner.on("\n ").skipNulls().join("",
|
||||
!added.isEmpty() ? ("ADDED:" + formatSetContents(added)) : null,
|
||||
!removed.isEmpty() ? ("REMOVED:" + formatSetContents(removed)) : null,
|
||||
"FINAL CONTENTS:" + formatSetContents(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted listing of Set contents, using a single line format if all elements are
|
||||
* wrappers of primitive types or Strings, and a multiline (one object per line) format if they
|
||||
* are not.
|
||||
*/
|
||||
private static String formatSetContents(Set<?> set) {
|
||||
for (Object obj : set) {
|
||||
if (!Primitives.isWrapperType(obj.getClass()) && !(obj instanceof String)) {
|
||||
return "\n " + Joiner.on(",\n ").join(set);
|
||||
}
|
||||
}
|
||||
return " " + set;
|
||||
}
|
||||
|
||||
private DiffUtils() {}
|
||||
}
|
118
util/src/main/java/google/registry/util/DomainNameUtils.java
Normal file
118
util/src/main/java/google/registry/util/DomainNameUtils.java
Normal file
|
@ -0,0 +1,118 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
|
||||
/** Utility methods related to domain names. */
|
||||
public final class DomainNameUtils {
|
||||
|
||||
/** Prefix for unicode domain name parts. */
|
||||
public static final String ACE_PREFIX = "xn--";
|
||||
public static final String ACE_PREFIX_REGEX = "^xn--";
|
||||
|
||||
/** Checks whether "name" is a strict subdomain of "potentialParent". */
|
||||
public static boolean isUnder(InternetDomainName name, InternetDomainName potentialParent) {
|
||||
int numNameParts = name.parts().size();
|
||||
int numParentParts = potentialParent.parts().size();
|
||||
return numNameParts > numParentParts
|
||||
&& name.parts().subList(numNameParts - numParentParts, numNameParts)
|
||||
.equals(potentialParent.parts());
|
||||
}
|
||||
|
||||
/** Canonicalizes a domain name by lowercasing and converting unicode to punycode. */
|
||||
public static String canonicalizeDomainName(String label) {
|
||||
String labelLowercased = Ascii.toLowerCase(label);
|
||||
try {
|
||||
return Idn.toASCII(labelLowercased);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(String.format("Error ASCIIfying label '%s'", label), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canonicalized TLD part of a valid fully-qualified domain name by stripping off the
|
||||
* leftmost part.
|
||||
*
|
||||
* <p>This method should not be called for subdomains.
|
||||
*
|
||||
* <p>This function is compatible with multi-part tlds, e.g. {@code co.uk}. This function will
|
||||
* also work on domains for which the registry is not authoritative. If you are certain that the
|
||||
* input will be under a TLD this registry controls, then it is preferable to use
|
||||
* {@link google.registry.model.registry.Registries#findTldForName(InternetDomainName)
|
||||
* Registries#findTldForName}, which will work on hostnames in addition to domains.
|
||||
*
|
||||
* @param fullyQualifiedDomainName must be a punycode SLD (not a host or unicode)
|
||||
* @throws IllegalArgumentException if there is no TLD
|
||||
*/
|
||||
public static String getTldFromDomainName(String fullyQualifiedDomainName) {
|
||||
checkArgument(
|
||||
!Strings.isNullOrEmpty(fullyQualifiedDomainName),
|
||||
"fullyQualifiedDomainName cannot be null or empty");
|
||||
return getTldFromDomainName(InternetDomainName.from(fullyQualifiedDomainName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canonicalized TLD part of a valid fully-qualified domain name by stripping off the
|
||||
* leftmost part.
|
||||
*
|
||||
* <p>This function is compatible with multi-part TLDs and should not be called with subdomains.
|
||||
*
|
||||
* @throws IllegalArgumentException if there is no TLD
|
||||
*/
|
||||
public static String getTldFromDomainName(InternetDomainName domainName) {
|
||||
checkArgumentNotNull(domainName);
|
||||
checkArgument(domainName.hasParent(), "domainName does not have a TLD");
|
||||
return domainName.parent().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the second level domain name for a fully qualified host name under a given tld.
|
||||
*
|
||||
* <p>This function is merely a string parsing utility, and does not verify if the tld is operated
|
||||
* by the registry.
|
||||
*
|
||||
* @throws IllegalArgumentException if either argument is null or empty, or the domain name is not
|
||||
* under the tld
|
||||
*/
|
||||
public static String getSecondLevelDomain(String hostName, String tld) {
|
||||
checkArgument(
|
||||
!Strings.isNullOrEmpty(hostName),
|
||||
"hostName cannot be null or empty");
|
||||
checkArgument(!Strings.isNullOrEmpty(tld), "tld cannot be null or empty");
|
||||
ImmutableList<String> domainParts = InternetDomainName.from(hostName).parts();
|
||||
ImmutableList<String> tldParts = InternetDomainName.from(tld).parts();
|
||||
checkArgument(
|
||||
domainParts.size() > tldParts.size(),
|
||||
"hostName must be at least one level below the tld");
|
||||
checkArgument(
|
||||
domainParts
|
||||
.subList(domainParts.size() - tldParts.size(), domainParts.size())
|
||||
.equals(tldParts),
|
||||
"hostName must be under the tld");
|
||||
ImmutableList<String> sldParts =
|
||||
domainParts.subList(domainParts.size() - tldParts.size() - 1, domainParts.size());
|
||||
return Joiner.on(".").join(sldParts);
|
||||
}
|
||||
|
||||
private DomainNameUtils() {}
|
||||
}
|
94
util/src/main/java/google/registry/util/EmailMessage.java
Normal file
94
util/src/main/java/google/registry/util/EmailMessage.java
Normal file
|
@ -0,0 +1,94 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.net.MediaType;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
/** Value class representing the content and metadata of an email. */
|
||||
@AutoValue
|
||||
public abstract class EmailMessage {
|
||||
|
||||
public abstract String subject();
|
||||
public abstract String body();
|
||||
public abstract ImmutableList<InternetAddress> recipients();
|
||||
public abstract InternetAddress from();
|
||||
public abstract Optional<InternetAddress> bcc();
|
||||
public abstract Optional<MediaType> contentType();
|
||||
public abstract Optional<Attachment> attachment();
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new AutoValue_EmailMessage.Builder();
|
||||
}
|
||||
|
||||
public static EmailMessage create(
|
||||
String subject, String body, InternetAddress recipient, InternetAddress from) {
|
||||
return newBuilder()
|
||||
.setSubject(subject)
|
||||
.setBody(body)
|
||||
.setRecipients(ImmutableList.of(recipient))
|
||||
.setFrom(from)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Builder for {@link EmailMessage}. */
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
|
||||
public abstract Builder setSubject(String subject);
|
||||
public abstract Builder setBody(String body);
|
||||
public abstract Builder setRecipients(Collection<InternetAddress> recipients);
|
||||
public abstract Builder setFrom(InternetAddress from);
|
||||
public abstract Builder setBcc(InternetAddress bcc);
|
||||
public abstract Builder setContentType(MediaType contentType);
|
||||
public abstract Builder setAttachment(Attachment attachment);
|
||||
|
||||
abstract ImmutableList.Builder<InternetAddress> recipientsBuilder();
|
||||
|
||||
public Builder addRecipient(InternetAddress value) {
|
||||
recipientsBuilder().add(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract EmailMessage build();
|
||||
}
|
||||
|
||||
/** An attachment to the email, if one exists. */
|
||||
@AutoValue
|
||||
public abstract static class Attachment {
|
||||
|
||||
public abstract MediaType contentType();
|
||||
public abstract String filename();
|
||||
public abstract String content();
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new AutoValue_EmailMessage_Attachment.Builder();
|
||||
}
|
||||
|
||||
/** Builder for {@link Attachment}. */
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
|
||||
public abstract Builder setContentType(MediaType contentType);
|
||||
public abstract Builder setFilename(String filename);
|
||||
public abstract Builder setContent(String content);
|
||||
public abstract Attachment build();
|
||||
}
|
||||
}
|
||||
}
|
32
util/src/main/java/google/registry/util/FixedClock.java
Normal file
32
util/src/main/java/google/registry/util/FixedClock.java
Normal file
|
@ -0,0 +1,32 @@
|
|||
// 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.util;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
/** A simple clock that always returns a fixed time. */
|
||||
public class FixedClock implements Clock {
|
||||
private final DateTime nowUtc;
|
||||
|
||||
public FixedClock(long millisSinceEpoch) {
|
||||
this.nowUtc = new DateTime(millisSinceEpoch, DateTimeZone.UTC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DateTime nowUtc() {
|
||||
return nowUtc;
|
||||
}
|
||||
}
|
229
util/src/main/java/google/registry/util/HexDumper.java
Normal file
229
util/src/main/java/google/registry/util/HexDumper.java
Normal file
|
@ -0,0 +1,229 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import javax.annotation.Nonnegative;
|
||||
import javax.annotation.WillNotClose;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
/**
|
||||
* Hex Dump Utility.
|
||||
*
|
||||
* <p>This class takes binary data and prints it out in a way that humans can read. It's just like
|
||||
* the hex dump programs you remembered as a kid. There a column on the left for the address (or
|
||||
* offset) in decimal, the middle columns are each digit in hexadecimal, and the column on the
|
||||
* right is the ASCII representation where non-printable characters are represented by {@code '.'}.
|
||||
*
|
||||
* <p>It's easiest to generate a simple {@link String} by calling {@link #dumpHex(byte[])}, or you
|
||||
* can stream data with {@link #HexDumper(Writer, int, int)}.
|
||||
*
|
||||
* <p>Example output:
|
||||
* <pre> {@code
|
||||
* [222 bytes total]
|
||||
* 00000000 90 0d 03 00 08 03 35 58 61 46 fd f3 f3 73 01 88 ......5XaF...s..
|
||||
* 00000016 cd 04 00 03 08 00 37 05 02 52 09 3f 8d 30 1c 45 ......7..R.?.0.E
|
||||
* 00000032 72 69 63 20 45 63 68 69 64 6e 61 20 28 74 65 73 ric Echidna (tes
|
||||
* 00000048 74 20 6b 65 79 29 20 3c 65 72 69 63 40 62 6f 75 t key) <eric@bou
|
||||
* 00000064 6e 63 79 63 61 73 74 6c 65 2e 6f 72 67 3e 00 0a ncycastle.org>..
|
||||
* 00000080 09 10 35 58 61 46 fd f3 f3 73 4b 5b 03 fe 2e 53 ..5XaF...sK[...S
|
||||
* 00000096 04 28 ab cb 35 3b e2 1b 63 91 65 3a 86 b9 fb 47 .(..5;..c.e:...G
|
||||
* 00000112 d5 4c 6a 21 50 f5 2e 39 76 aa d5 86 d7 96 3b 9a .Lj!P..9v.....;.
|
||||
* 00000128 1a c3 6d c0 50 7f c6 25 9a 04 de 0f 1f 20 ae 70 ..m.P..%..... .p
|
||||
* 00000144 f9 77 c4 8b bf ec 3c 2f 59 58 b8 47 81 6a 59 25 .w....</YX.G.jY%
|
||||
* 00000160 82 b0 ba e2 a9 43 94 aa fc 92 2b b3 76 77 f5 ba .....C....+.vw..
|
||||
* 00000176 5b 59 9a de 22 1c 79 06 88 d2 ba 97 51 e3 11 2e [Y..".y.....Q...
|
||||
* 00000192 5b c0 c6 8c 34 4d a7 28 77 bf 11 27 e7 6c 8e 1c [...4M.(w..'.l..
|
||||
* 00000208 b4 a6 66 18 8e 69 3c 18 b7 97 d5 34 9a bb ..f..i<....4..
|
||||
* }</pre>
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public final class HexDumper extends OutputStream {
|
||||
|
||||
@Nonnegative
|
||||
public static final int DEFAULT_PER_LINE = 16;
|
||||
|
||||
@Nonnegative
|
||||
public static final int DEFAULT_PER_GROUP = 4;
|
||||
|
||||
private Writer upstream;
|
||||
|
||||
@Nonnegative
|
||||
private final int perLine;
|
||||
|
||||
@Nonnegative
|
||||
private final int perGroup;
|
||||
|
||||
private long totalBytes;
|
||||
|
||||
@Nonnegative
|
||||
private int lineCount;
|
||||
|
||||
private StringBuilder line;
|
||||
|
||||
private final char[] asciis;
|
||||
|
||||
/**
|
||||
* Calls {@link #dumpHex(byte[], int, int)} with {@code perLine} set to
|
||||
* {@value #DEFAULT_PER_LINE} and {@code perGroup} set to {@value #DEFAULT_PER_GROUP}.
|
||||
*/
|
||||
public static String dumpHex(byte[] data) {
|
||||
return dumpHex(data, DEFAULT_PER_LINE, DEFAULT_PER_GROUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience static method for generating a hex dump as a {@link String}.
|
||||
*
|
||||
* <p>This method adds an additional line to the beginning with the total number of bytes.
|
||||
*
|
||||
* @see #HexDumper(Writer, int, int)
|
||||
*/
|
||||
public static String dumpHex(byte[] data, @Nonnegative int perLine, @Nonnegative int perGroup) {
|
||||
checkNotNull(data, "data");
|
||||
StringWriter writer = new StringWriter();
|
||||
writer.write(String.format("[%d bytes total]\n", data.length));
|
||||
try (HexDumper hexDump = new HexDumper(writer, perLine, perGroup)) {
|
||||
hexDump.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link #HexDumper(Writer, int, int)} with {@code perLine} set to
|
||||
* {@value #DEFAULT_PER_LINE} and {@code perGroup} set to {@value #DEFAULT_PER_GROUP}.
|
||||
*/
|
||||
public HexDumper(@WillNotClose Writer writer) {
|
||||
this(writer, DEFAULT_PER_LINE, DEFAULT_PER_GROUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new streaming {@link HexDumper} object.
|
||||
*
|
||||
* <p>The output is line-buffered so a single write call is made to {@code out} for each line.
|
||||
* This is done to avoid system call overhead {@code out} is a resource, and reduces the chance
|
||||
* of lines being broken by other threads writing to {@code out} (or its underlying resource) at
|
||||
* the same time.
|
||||
*
|
||||
* <p>This object will <i>not</i> close {@code out}. You must close <i>both</i> this object and
|
||||
* {@code out}, and this object must be closed <i>first</i>.
|
||||
*
|
||||
* @param out is the stream to which the hex dump text is written. It is <i>not</i> closed.
|
||||
* @param perLine determines how many hex characters to show on each line.
|
||||
* @param perGroup how many columns of hex digits should be grouped together. If this value is
|
||||
* {@code > 0}, an extra space will be inserted after each Nth column for readability.
|
||||
* Grouping can be disabled by setting this to {@code 0}.
|
||||
* @see #dumpHex(byte[])
|
||||
*/
|
||||
public HexDumper(@WillNotClose Writer out, @Nonnegative int perLine, @Nonnegative int perGroup) {
|
||||
checkArgument(0 < perLine, "0 < perLine <= INT32_MAX");
|
||||
checkArgument(0 <= perGroup && perGroup < perLine, "0 <= perGroup < perLine");
|
||||
this.upstream = checkNotNull(out, "out");
|
||||
this.totalBytes = 0L;
|
||||
this.perLine = perLine;
|
||||
this.perGroup = perGroup;
|
||||
this.asciis = new char[perLine];
|
||||
this.line = newLine();
|
||||
}
|
||||
|
||||
/** Initializes member variables at the beginning of a new line. */
|
||||
private StringBuilder newLine() {
|
||||
lineCount = 0;
|
||||
return new StringBuilder(String.format("%08d ", totalBytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single byte to the current line buffer, flushing if end of line.
|
||||
*
|
||||
* @throws IOException upon failure to write to upstream {@link Writer#write(String) writer}.
|
||||
* @throws IllegalStateException if this object has been {@link #close() closed}.
|
||||
*/
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
String flush = null;
|
||||
line.append(String.format("%02x ", (byte) b));
|
||||
asciis[lineCount] = b >= 32 && b <= 126 ? (char) b : '.';
|
||||
++lineCount;
|
||||
++totalBytes;
|
||||
if (lineCount == perLine) {
|
||||
line.append(' ');
|
||||
line.append(asciis);
|
||||
line.append('\n');
|
||||
flush = line.toString();
|
||||
line = newLine();
|
||||
} else {
|
||||
if (perGroup > 0 && lineCount % perGroup == 0) {
|
||||
line.append(' ');
|
||||
}
|
||||
}
|
||||
// Writing upstream is deferred until the end in order to *somewhat* maintain a correct
|
||||
// internal state in the event that an exception is thrown. This also avoids the need for
|
||||
// a try statement which usually makes code run slower.
|
||||
if (flush != null) {
|
||||
upstream.write(flush);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes partial line buffer (if any) to upstream {@link Writer}.
|
||||
*
|
||||
* @throws IOException upon failure to write to upstream {@link Writer#write(String) writer}.
|
||||
* @throws IllegalStateException if this object has been {@link #close() closed}.
|
||||
*/
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (line.length() > 0) {
|
||||
upstream.write(line.toString());
|
||||
line = new StringBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out the final line (if incomplete) and invalidates this object.
|
||||
*
|
||||
* <p>This object must be closed <i>before</i> you close the upstream writer. Please note that
|
||||
* this method <i>does not</i> close upstream writer for you.
|
||||
*
|
||||
* <p>If you attempt to write to this object after calling this method,
|
||||
* {@link IllegalStateException} will be thrown. However, it's safe to call close multiple times,
|
||||
* as subsequent calls will be treated as a no-op.
|
||||
*
|
||||
* @throws IOException upon failure to write to upstream {@link Writer#write(String) writer}.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (lineCount > 0) {
|
||||
while (lineCount < perLine) {
|
||||
asciis[lineCount] = ' ';
|
||||
line.append(" ");
|
||||
++lineCount;
|
||||
if (perGroup > 0 && lineCount % perGroup == 0 && lineCount != perLine) {
|
||||
line.append(' ');
|
||||
}
|
||||
}
|
||||
line.append(' ');
|
||||
line.append(asciis);
|
||||
line.append('\n');
|
||||
flush();
|
||||
}
|
||||
}
|
||||
}
|
77
util/src/main/java/google/registry/util/Idn.java
Normal file
77
util/src/main/java/google/registry/util/Idn.java
Normal file
|
@ -0,0 +1,77 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.ibm.icu.text.IDNA;
|
||||
import com.ibm.icu.text.IDNA.Info;
|
||||
|
||||
/**
|
||||
* A partial API-compatible replacement for {@link java.net.IDN} that replaces <a
|
||||
* href="http://www.ietf.org/rfc/rfc3490.txt">IDNA2003</a> processing with <a
|
||||
* href="http://unicode.org/reports/tr46/">UTS46 transitional processing</a>, with differences as
|
||||
* described in the <a href="http://www.unicode.org/reports/tr46/#IDNAComparison">UTS46
|
||||
* documentation</a>/
|
||||
*
|
||||
* <p>This class provides methods to convert internationalized domain names (IDNs) between Unicode
|
||||
* and ASCII-only <a href="http://www.ietf.org/rfc/rfc3492.txt">Punycode</a> form. It implements the
|
||||
* parts of the API from {@link java.net.IDN} that we care about, but will return different results
|
||||
* as defined by the differences between IDNA2003 and transitional UTS46.
|
||||
*/
|
||||
public final class Idn {
|
||||
|
||||
/** Cached UTS46 with the flags we want. */
|
||||
private static final IDNA UTS46_INSTANCE = IDNA.getUTS46Instance(IDNA.CHECK_BIDI);
|
||||
|
||||
/**
|
||||
* Translates a string from Unicode to ASCII Compatible Encoding (ACE), as defined by the ToASCII
|
||||
* operation of <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>.
|
||||
*
|
||||
* <p>This method always uses <a href="http://unicode.org/reports/tr46/">UTS46 transitional
|
||||
* processing</a>.
|
||||
*
|
||||
* @param name a domain name, which may include multiple labels separated by dots
|
||||
* @throws IllegalArgumentException if the input string doesn't conform to RFC 3490 specification
|
||||
* @see java.net.IDN#toASCII(String)
|
||||
*/
|
||||
public static String toASCII(String name) {
|
||||
Info info = new Info();
|
||||
StringBuilder result = new StringBuilder();
|
||||
UTS46_INSTANCE.nameToASCII(name, result, info);
|
||||
if (info.hasErrors()) {
|
||||
throw new IllegalArgumentException("Errors: " + Joiner.on(',').join(info.getErrors()));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a string from ASCII Compatible Encoding (ACE) to Unicode, as defined by the
|
||||
* ToUnicode operation of <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>.
|
||||
*
|
||||
* <p>This method always uses <a href="http://unicode.org/reports/tr46/">UTS46 transitional
|
||||
* processing</a>.
|
||||
*
|
||||
* <p>ToUnicode never fails. In case of any error, the input string is returned unmodified.
|
||||
*
|
||||
* @param name a domain name, which may include multiple labels separated by dots
|
||||
* @see java.net.IDN#toUnicode(String)
|
||||
*/
|
||||
public static String toUnicode(String name) {
|
||||
Info info = new Info();
|
||||
StringBuilder result = new StringBuilder();
|
||||
UTS46_INSTANCE.nameToUnicode(name, result, info);
|
||||
return info.hasErrors() ? name : result.toString();
|
||||
}
|
||||
}
|
145
util/src/main/java/google/registry/util/ImprovedInputStream.java
Normal file
145
util/src/main/java/google/registry/util/ImprovedInputStream.java
Normal file
|
@ -0,0 +1,145 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
||||
import javax.annotation.WillCloseWhenClosed;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
/**
|
||||
* {@link InputStream} wrapper that offers some additional magic.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Byte counting
|
||||
* <li>Log byte count on close
|
||||
* <li>Check expected byte count when closed (Optional)
|
||||
* <li>Close original {@link InputStream} when closed (Optional)
|
||||
* <li>Overridable {@link #onClose()} method
|
||||
* <li>Throws {@link NullPointerException} if read after {@link #close()}
|
||||
* </ul>
|
||||
*
|
||||
* @see ImprovedOutputStream
|
||||
* @see com.google.common.io.CountingInputStream
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public class ImprovedInputStream extends FilterInputStream {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private long count;
|
||||
private long mark = -1;
|
||||
private final boolean shouldClose;
|
||||
private final String name;
|
||||
|
||||
public ImprovedInputStream(String name, @WillCloseWhenClosed InputStream out) {
|
||||
this(name, out, true);
|
||||
}
|
||||
|
||||
public ImprovedInputStream(String name, InputStream in, boolean shouldClose) {
|
||||
super(checkNotNull(in, "in"));
|
||||
this.shouldClose = shouldClose;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
@OverridingMethodsMustInvokeSuper
|
||||
public int read() throws IOException {
|
||||
int result = in.read();
|
||||
if (result != -1) {
|
||||
count++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int read(byte[] b) throws IOException {
|
||||
return this.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
@OverridingMethodsMustInvokeSuper
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int result = in.read(b, off, len);
|
||||
if (result != -1) {
|
||||
count += result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long result = in.skip(n);
|
||||
count += result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
in.mark(readlimit);
|
||||
mark = count;
|
||||
// it's okay to mark even if mark isn't supported, as reset won't work
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
if (!in.markSupported()) {
|
||||
throw new IOException("Mark not supported");
|
||||
}
|
||||
if (mark == -1) {
|
||||
throw new IOException("Mark not set");
|
||||
}
|
||||
|
||||
in.reset();
|
||||
count = mark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs byte count, checks byte count (optional), closes (optional), and self-sabotages.
|
||||
*
|
||||
* <p>This method may not be overridden, use {@link #onClose()} instead.
|
||||
*
|
||||
* @see InputStream#close()
|
||||
*/
|
||||
@Override
|
||||
@NonFinalForTesting
|
||||
public void close() throws IOException {
|
||||
if (in == null) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
if (shouldClose) {
|
||||
in.close();
|
||||
}
|
||||
in = null;
|
||||
logger.atInfo().log("%s closed with %,d bytes read", name, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable method that's called by {@link #close()}.
|
||||
*
|
||||
* <p>This method does nothing by default.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
protected void onClose() throws IOException {
|
||||
// Does nothing by default.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
||||
import javax.annotation.WillCloseWhenClosed;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
/**
|
||||
* {@link OutputStream} wrapper that offers some additional magic.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Byte counting
|
||||
* <li>Always {@link #flush()} on {@link #close()}
|
||||
* <li>Check expected byte count when closed (Optional)
|
||||
* <li>Close original {@link OutputStream} when closed (Optional)
|
||||
* <li>Overridable {@link #onClose()} method
|
||||
* <li>Throws {@link NullPointerException} if written after {@link #close()}
|
||||
* </ul>
|
||||
*
|
||||
* @see ImprovedInputStream
|
||||
* @see com.google.common.io.CountingOutputStream
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public class ImprovedOutputStream extends FilterOutputStream {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private long count;
|
||||
private final boolean shouldClose;
|
||||
private final String name;
|
||||
|
||||
public ImprovedOutputStream(String name, @WillCloseWhenClosed OutputStream out) {
|
||||
this(name, out, true);
|
||||
}
|
||||
|
||||
public ImprovedOutputStream(String name, OutputStream out, boolean shouldClose) {
|
||||
super(checkNotNull(out, "out"));
|
||||
this.shouldClose = shouldClose;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** Returns the number of bytes that have been written to this stream thus far. */
|
||||
public long getBytesWritten() {
|
||||
return count;
|
||||
}
|
||||
|
||||
/** @see java.io.FilterOutputStream#write(int) */
|
||||
@Override
|
||||
@OverridingMethodsMustInvokeSuper
|
||||
public void write(int b) throws IOException {
|
||||
out.write(b);
|
||||
++count;
|
||||
}
|
||||
|
||||
/** @see #write(byte[], int, int) */
|
||||
@Override
|
||||
public final void write(byte[] b) throws IOException {
|
||||
this.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
/** @see java.io.FilterOutputStream#write(byte[], int, int) */
|
||||
@Override
|
||||
@OverridingMethodsMustInvokeSuper
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
out.write(b, off, len);
|
||||
count += len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes, logs byte count, checks byte count (optional), closes (optional), and self-sabotages.
|
||||
*
|
||||
* <p>This method may not be overridden, use {@link #onClose()} instead.
|
||||
*
|
||||
* @see java.io.OutputStream#close()
|
||||
*/
|
||||
@Override
|
||||
@NonFinalForTesting
|
||||
public void close() throws IOException {
|
||||
if (out == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
flush();
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("flush() failed for %s", name);
|
||||
}
|
||||
onClose();
|
||||
if (shouldClose) {
|
||||
out.close();
|
||||
}
|
||||
out = null;
|
||||
logger.atInfo().log("%s closed with %,d bytes written", name, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable method that's called by {@link #close()}.
|
||||
*
|
||||
* <p>This method does nothing by default.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
protected void onClose() throws IOException {
|
||||
// Does nothing by default.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
|
||||
/**
|
||||
* Creates {@link CharMatcher CharMatchers} that support Java character strings only, not unicode
|
||||
* supplementary characters.
|
||||
*/
|
||||
public class JavaCharMatchers {
|
||||
|
||||
/** Returns a {@link CharMatcher} that matcher only ASCII letters and digits. */
|
||||
public static CharMatcher asciiLetterOrDigitMatcher() {
|
||||
return CharMatcher.inRange('0', '9')
|
||||
.or(CharMatcher.inRange('a', 'z'))
|
||||
.or(CharMatcher.inRange('A', 'Z'));
|
||||
}
|
||||
|
||||
private JavaCharMatchers() {}
|
||||
}
|
29
util/src/main/java/google/registry/util/ListNamingUtils.java
Normal file
29
util/src/main/java/google/registry/util/ListNamingUtils.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* A utility class for conversion of input file paths into names for entities in Datastore.
|
||||
*/
|
||||
public final class ListNamingUtils {
|
||||
|
||||
/** Turns a file path into a name suitable for use as the name of a premium or reserved list. */
|
||||
public static String convertFilePathToName(Path file) {
|
||||
return Files.getNameWithoutExtension(file.getFileName().toString());
|
||||
}
|
||||
}
|
134
util/src/main/java/google/registry/util/NetworkUtils.java
Normal file
134
util/src/main/java/google/registry/util/NetworkUtils.java
Normal file
|
@ -0,0 +1,134 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.CollectionUtils.union;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.io.IOException;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
|
||||
/** Utilities for networking. */
|
||||
public final class NetworkUtils {
|
||||
|
||||
private static final int PICK_ATTEMPTS = 16;
|
||||
private static final int RANDOM_PORT_BASE = 32768;
|
||||
private static final int RANDOM_PORT_RANGE = 60000 - RANDOM_PORT_BASE + 1;
|
||||
|
||||
@GuardedBy("random")
|
||||
private static final Random random = new SecureRandom();
|
||||
|
||||
/**
|
||||
* Returns random unused local port that can be used for TCP listening server.
|
||||
*
|
||||
* @throws RuntimeException if failed to find free port after {@value #PICK_ATTEMPTS} attempts
|
||||
*/
|
||||
public static int pickUnusedPort() {
|
||||
// In an ideal world, we would just listen on port 0 and use whatever random port the kernel
|
||||
// assigns us. But our CI testing system reports there are rare circumstances in which this
|
||||
// doesn't work.
|
||||
Iterator<Integer> ports = union(generateRandomPorts(PICK_ATTEMPTS), 0).iterator();
|
||||
while (true) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(ports.next())) {
|
||||
return serverSocket.getLocalPort();
|
||||
} catch (IOException e) {
|
||||
if (!ports.hasNext()) {
|
||||
throw new RuntimeException("Failed to acquire random port", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified domain name of the local host in all lower case.
|
||||
*
|
||||
* @throws RuntimeException to wrap {@link UnknownHostException} if the local host could not be
|
||||
* resolved into an address
|
||||
*/
|
||||
public static String getCanonicalHostName() {
|
||||
try {
|
||||
return Ascii.toLowerCase(getExternalAddressOfLocalSystem().getCanonicalHostName());
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the externally-facing IPv4 network address of the local host.
|
||||
*
|
||||
* <p>This function implements a workaround for an <a
|
||||
* href="http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4665037">issue</a> in {@link
|
||||
* InetAddress#getLocalHost}.
|
||||
*
|
||||
* <p><b>Note:</b> This code was pilfered from {@link "com.google.net.base.LocalHost"} which was
|
||||
* never made open source.
|
||||
*
|
||||
* @throws UnknownHostException if the local host could not be resolved into an address
|
||||
*/
|
||||
public static InetAddress getExternalAddressOfLocalSystem() throws UnknownHostException {
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
// If we have a loopback address, look for an address using the network cards.
|
||||
if (localhost.isLoopbackAddress()) {
|
||||
try {
|
||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
if (interfaces == null) {
|
||||
return localhost;
|
||||
}
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = interfaces.nextElement();
|
||||
Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
|
||||
while (addresses.hasMoreElements()) {
|
||||
InetAddress address = addresses.nextElement();
|
||||
if (!(address.isLoopbackAddress()
|
||||
|| address.isLinkLocalAddress()
|
||||
|| address instanceof Inet6Address)) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
// Fall-through.
|
||||
}
|
||||
}
|
||||
return localhost;
|
||||
}
|
||||
|
||||
private static ImmutableSet<Integer> generateRandomPorts(int count) {
|
||||
checkArgument(count > 0);
|
||||
Set<Integer> result = new HashSet<>();
|
||||
synchronized (random) {
|
||||
while (result.size() < count) {
|
||||
result.add(RANDOM_PORT_BASE + random.nextInt(RANDOM_PORT_RANGE));
|
||||
}
|
||||
}
|
||||
return ImmutableSet.copyOf(result);
|
||||
}
|
||||
|
||||
private NetworkUtils() {}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// 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.util;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation that discourages future maintainers from marking a field {@code final}.
|
||||
*
|
||||
* <p>This annotation serves purely as documention to indicate that even though a {@code private}
|
||||
* field may <em>appear</em> safe to change to {@code final}, it will actually be reflectively
|
||||
* modified by a unit test, and therefore should not be {@code final}.
|
||||
*
|
||||
* <p>When this annotation is used on methods, it means that you should not override the method
|
||||
* and it's only non-{@code final} so it can be mocked.
|
||||
*
|
||||
* @see google.registry.testing.InjectRule
|
||||
*/
|
||||
@Documented
|
||||
@Retention(SOURCE)
|
||||
@Target({FIELD, METHOD, TYPE})
|
||||
public @interface NonFinalForTesting {}
|
509
util/src/main/java/google/registry/util/PosixTarHeader.java
Normal file
509
util/src/main/java/google/registry/util/PosixTarHeader.java
Normal file
|
@ -0,0 +1,509 @@
|
|||
// 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.util;
|
||||
|
||||
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.Strings.isNullOrEmpty;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.joda.time.DateTimeConstants.MILLIS_PER_SECOND;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* POSIX Tar Header.
|
||||
*
|
||||
* <p>This class represents the 512-byte header that precedes each file within a tar file archive.
|
||||
* It's in the POSIX ustar format which is equivalent to using the following GNU tar flags:
|
||||
* {@code tar --format=ustar}. It's called ustar because you're a star!
|
||||
*
|
||||
* <p><b>Warning:</b> This class is not a complete tar implementation. It also offers no
|
||||
* abstractions beyond the header format and has only been tested against very simple archives
|
||||
* created in the ustar format and the default format for gnu and bsd tar. If your goal is to be
|
||||
* able to read generic tar files, you should use a more mature tar implementation like
|
||||
* <a href="http://commons.apache.org/proper/commons-compress/">Apache Commons Compress</a>.
|
||||
*
|
||||
* <p>This class is only really useful in situations where the following statements are true:
|
||||
*
|
||||
* <ol>
|
||||
* <li>You want to <i>create</i> tar archives.
|
||||
* <li>You don't need to be able to read tar files from external sources.
|
||||
* <li>You don't want additional dependencies.
|
||||
* <li>You don't need fancy features like symbolic links.
|
||||
* <li>You want something that's a step above writing out the bytes by hand.
|
||||
* </ol>
|
||||
*
|
||||
* <p>To create a tar archive using this class, you must do the following: For each file in the
|
||||
* archive, output a header and the file contents ({@code null}-padded to the nearest 512-byte
|
||||
* boundary). Then output another 1024 {@code null} bytes to indicate end of archive.
|
||||
*
|
||||
* <p>The ustar tar header contains the following fields:
|
||||
*
|
||||
* <dl>
|
||||
* <dt>name<dd><i>[Offset: 0, Length: 100]</i><br>
|
||||
* C String which we'll assume is UTF-8 (Offset: 0)
|
||||
* <dt>mode<dd><i>[Offset: 100, Length: 8]</i><br>
|
||||
* ASCII 7-digit zero-padded octal file mode and {@code null} byte.
|
||||
* <dt>uid<dd><i>[Offset: 108, Length: 8]</i><br>
|
||||
* ASCII 7-digit zero-padded octal UNIX user ID and {@code null} byte.
|
||||
* <dt>gid<dd><i>[Offset: 116, Length: 8]</i><br>
|
||||
* ASCII 7-digit zero-padded octal UNIX group ID and {@code null} byte.
|
||||
* <dt>size<dd><i>[Offset: 124, Length: 12]</i><br>
|
||||
* ASCII 11-digit zero-padded octal file size and {@code null} byte.
|
||||
* <dt>mtime<dd><i>[Offset: 136, Length: 12]</i><br>
|
||||
* ASCII octal UNIX timestamp modified time and {@code null} byte.
|
||||
* <dt>chksum<dd><i>[Offset: 148, Length: 8]</i><br>
|
||||
* ASCII octal sum of all header bytes where chksum are 0's.
|
||||
* <dt>typeflag<dd><i>[Offset: 156]</i><br>
|
||||
* Always {@code '0'} (zero character) for regular type of file.
|
||||
* <dt>linkname<dd><i>[Offset: 157, Length: 100]</i><br>
|
||||
* All {@code null} bytes because we don't support symbolic links.
|
||||
* <dt>magic<dd><i>[Offset: 257, Length: 6]</i><br>
|
||||
* Always the C string "ustar".
|
||||
* <dt>version<dd><i>[Offset: 263, Length: 2]</i><br>
|
||||
* Always "00" without a {@code null} or blank on GNU systems.
|
||||
* <dt>uname<dd><i>[Offset: 265, Length: 32]</i><br>
|
||||
* The C string UNIX user name corresponding to {@code uid}.
|
||||
* <dt>gname<dd><i>[Offset: 297, Length: 32]</i><br>
|
||||
* The C string UNIX group name corresponding to {@code gid}.
|
||||
* <dt>devmajor<dd><i>[Offset: 329, Length: 8]</i><br>
|
||||
* Not supported; set to zero.
|
||||
* <dt>devminor<dd><i>[Offset: 337, Length: 8]</i><br>
|
||||
* Not supported; set to zero.
|
||||
* <dt>prefix<dd><i>[Offset: 345, Length: 155]</i><br>
|
||||
* Not supported; set to {@code null}.
|
||||
* </dl>
|
||||
*
|
||||
* @see <a href="http://www.gnu.org/software/tar/manual/html_node/Standard.html">Tar Standard</a>
|
||||
*/
|
||||
@Immutable
|
||||
public final class PosixTarHeader {
|
||||
|
||||
/** Type of file stored in the archive. Only normal files and directories are supported. */
|
||||
public enum Type {
|
||||
/** A regular file. This can't be a symbolic link or anything interesting. */
|
||||
REGULAR,
|
||||
|
||||
/** A directory AKA folder. */
|
||||
DIRECTORY,
|
||||
|
||||
/** This indicates we read a file from an archive with an unsupported type. */
|
||||
UNSUPPORTED
|
||||
}
|
||||
|
||||
public static final int HEADER_LENGTH = 512;
|
||||
|
||||
private final byte[] header;
|
||||
|
||||
/**
|
||||
* Create a new header from the bytes of an existing tar file header (Safe).
|
||||
*
|
||||
* <p>This routine validates the checksum to ensure the data header is correct and supported.
|
||||
*
|
||||
* @param header the existing and assumed correct header. Value is defensively copied.
|
||||
* @throws IllegalArgumentException if header isn't ustar or has a bad checksum.
|
||||
* @throws NullPointerException if {@code header} is {@code null}.
|
||||
*/
|
||||
public static PosixTarHeader from(byte[] header) {
|
||||
checkNotNull(header, "header");
|
||||
checkArgument(header.length == HEADER_LENGTH,
|
||||
"POSIX tar header length should be %s but was %s", HEADER_LENGTH, header.length);
|
||||
PosixTarHeader res = new PosixTarHeader(header.clone());
|
||||
checkArgument(res.getMagic().equals("ustar"),
|
||||
"Not a POSIX tar ustar header.");
|
||||
String version = res.getVersion();
|
||||
checkArgument(version.isEmpty() || version.equals("00"),
|
||||
"Only POSIX tar ustar version 00 and the GNU variant supported.");
|
||||
checkArgument(res.getChksum() == checksum(header),
|
||||
"POSIX tar header chksum invalid.");
|
||||
return res;
|
||||
}
|
||||
|
||||
/** Constructs a new instance (Unsafe). */
|
||||
PosixTarHeader(byte[] header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
/** Returns 512-byte tar header (safe copy). */
|
||||
public byte[] getBytes() {
|
||||
return header.clone();
|
||||
}
|
||||
|
||||
/** Returns the filename. */
|
||||
public String getName() {
|
||||
return extractField(0, 100);
|
||||
}
|
||||
|
||||
/** Returns the octal UNIX mode aka permissions. */
|
||||
public int getMode() {
|
||||
return Integer.parseInt(extractField(100, 8).trim(), 8);
|
||||
}
|
||||
|
||||
/** Returns the UNIX owner/user id. */
|
||||
public int getUid() {
|
||||
return Integer.parseInt(extractField(108, 8).trim(), 8);
|
||||
}
|
||||
|
||||
/** Returns the UNIX group id. */
|
||||
public int getGid() {
|
||||
return Integer.parseInt(extractField(116, 8).trim(), 8);
|
||||
}
|
||||
|
||||
/** Returns the file size in bytes. */
|
||||
public int getSize() {
|
||||
return Integer.parseInt(extractField(124, 12).trim(), 8);
|
||||
}
|
||||
|
||||
/** Returns the modified time as a UTC {@link DateTime} object. */
|
||||
public DateTime getMtime() {
|
||||
return new DateTime(Long.parseLong(extractField(136, 12).trim(), 8) * MILLIS_PER_SECOND, UTC);
|
||||
}
|
||||
|
||||
/** Returns the checksum value stored in the . */
|
||||
public int getChksum() {
|
||||
return Integer.parseInt(extractField(148, 8).trim(), 8);
|
||||
}
|
||||
|
||||
/** Returns the {@link Type} of file. */
|
||||
public Type getType() {
|
||||
switch (header[156]) {
|
||||
case '\0':
|
||||
case '0':
|
||||
return Type.REGULAR;
|
||||
case '5':
|
||||
return Type.DIRECTORY;
|
||||
default:
|
||||
return Type.UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UNIX symbolic link name.
|
||||
*
|
||||
* <p>This feature is unsupported but the getter is included for completeness.
|
||||
*/
|
||||
public String getLinkName() {
|
||||
return extractField(157, 100);
|
||||
}
|
||||
|
||||
/** Returns the {@code magic} field. Only {@code "ustar"} is supported. */
|
||||
public String getMagic() {
|
||||
return extractField(257, 6).trim();
|
||||
}
|
||||
|
||||
/** Returns the {@code magic} field. Only {@code "00"} is supported. */
|
||||
public String getVersion() {
|
||||
return extractField(263, 2).trim();
|
||||
}
|
||||
|
||||
/** Returns the UNIX user name (or owner) of the file. */
|
||||
public String getUname() {
|
||||
return extractField(265, 32).trim();
|
||||
}
|
||||
|
||||
/** Returns the UNIX group name associated with the file. */
|
||||
public String getGname() {
|
||||
return extractField(297, 32).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code devmajor} field.
|
||||
*
|
||||
* <p>This feature is unsupported but the getter is included for completeness.
|
||||
*/
|
||||
public String getDevMajor() {
|
||||
return extractField(329, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code devminor} field.
|
||||
*
|
||||
* <p>This feature is unsupported but the getter is included for completeness.
|
||||
*/
|
||||
public String getDevMinor() {
|
||||
return extractField(337, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code prefix} field.
|
||||
*
|
||||
* <p>This feature is unsupported but the getter is included for completeness.
|
||||
*/
|
||||
public String getPrefix() {
|
||||
return extractField(345, 155);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a C string field. This routine is lenient when it comes to the terminating
|
||||
* {@code null} byte. If it's not present, it'll be assumed that the string length is the
|
||||
* size of the field. The encoding is always assumed to be UTF-8.
|
||||
*/
|
||||
private String extractField(int offset, int max) {
|
||||
return new String(header, offset, extractFieldLength(offset, max), UTF_8);
|
||||
}
|
||||
|
||||
/** Returns length of C string in field, or {@code max} if there's no {@code null} byte. */
|
||||
private int extractFieldLength(int offset, int max) {
|
||||
for (int n = 0; n < max; ++n) {
|
||||
if (header[offset + n] == '\0') {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/** @see Arrays#hashCode(byte[]) */
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(header);
|
||||
}
|
||||
|
||||
/** @see Arrays#equals(byte[], byte[]) */
|
||||
@Override
|
||||
public boolean equals(@Nullable Object rhs) {
|
||||
return rhs == this
|
||||
|| (rhs != null
|
||||
&& getClass() == rhs.getClass()
|
||||
&& Arrays.equals(header, ((PosixTarHeader) rhs).header));
|
||||
}
|
||||
|
||||
/** @see Arrays#toString(byte[]) */
|
||||
@Override
|
||||
public String toString() {
|
||||
return Arrays.toString(header);
|
||||
}
|
||||
|
||||
/** Simple checksum algorithm specified by tar. */
|
||||
static int checksum(byte[] bytes) {
|
||||
int sum = 0;
|
||||
for (int n = 0; n < 148; ++n) {
|
||||
sum += bytes[n];
|
||||
}
|
||||
sum += ' ' * 8; // We pretend the chksum field stores spaces.
|
||||
for (int n = 148 + 8; n < bytes.length; ++n) {
|
||||
sum += bytes[n];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link PosixTarHeader}.
|
||||
*
|
||||
* <p>The following fields are required:<ul>
|
||||
* <li>{@link #setName(String)}
|
||||
* <li>{@link #setSize(long)}</ul>
|
||||
*
|
||||
* <p>{@link #build()} may be called multiple times. With the exception of the required fields
|
||||
* listed above, fields will retain the values. This is useful if you want to construct many
|
||||
* file headers with the same value for certain certain fields (e.g. uid, gid, uname, gname)
|
||||
* but don't want to have to call their setters repeatedly.
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private static final int DEFAULT_MODE = 0640;
|
||||
private static final int DEFAULT_UID = 0;
|
||||
private static final int DEFAULT_GID = 0;
|
||||
private static final String DEFAULT_UNAME = "root";
|
||||
private static final String DEFAULT_GNAME = "wheel";
|
||||
private static final Type DEFAULT_TYPE = Type.REGULAR;
|
||||
|
||||
private final byte[] header = new byte[HEADER_LENGTH];
|
||||
private boolean hasName = false;
|
||||
private boolean hasSize = false;
|
||||
|
||||
public Builder() {
|
||||
setMode(DEFAULT_MODE);
|
||||
setUid(DEFAULT_UID);
|
||||
setGid(DEFAULT_GID);
|
||||
setMtime(new DateTime(UTC));
|
||||
setType(DEFAULT_TYPE);
|
||||
setMagic();
|
||||
setVersion();
|
||||
setUname(DEFAULT_UNAME);
|
||||
setGname(DEFAULT_GNAME);
|
||||
setField("devmajor", 329, 8, "0000000"); // I have no clue what this is.
|
||||
setField("devminor", 337, 8, "0000000"); // I have no clue what this is.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file name. (Required)
|
||||
*
|
||||
* @param name must be {@code <100} characters in length.
|
||||
*/
|
||||
public Builder setName(String name) {
|
||||
checkArgument(!isNullOrEmpty(name), "name");
|
||||
setField("name", 0, 100, name);
|
||||
hasName = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UNIX file mode aka permissions. By default this is {@value #DEFAULT_MODE}.
|
||||
*
|
||||
* @param mode <u>This value is octal</u>. Just in case you were wondering, {@code 416} is the
|
||||
* decimal representation of {@code 0640}. If that number doesn't look familiar to you,
|
||||
* search Google for "chmod". The value must be {@code >=0} and {@code <8^7}.
|
||||
*/
|
||||
public Builder setMode(int mode) {
|
||||
checkArgument(0 <= mode && mode <= 07777777,
|
||||
"Tar mode out of range: %s", mode);
|
||||
setField("mode", 100, 8, String.format("%07o", mode));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the owner's UNIX user ID. By default this is {@value #DEFAULT_UID}.
|
||||
*
|
||||
* @param uid must be {@code >=0} and {@code <8^7}.
|
||||
* @see #setUname(String)
|
||||
*/
|
||||
public Builder setUid(int uid) {
|
||||
checkArgument(0 <= uid && uid <= 07777777,
|
||||
"Tar uid out of range: %s", uid);
|
||||
setField("uid", 108, 8, String.format("%07o", uid));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UNIX group ID. By default this is {@value #DEFAULT_GID}.
|
||||
*
|
||||
* @param gid must be {@code >=0} and {@code <8^7}.
|
||||
* @see #setGname(String)
|
||||
*/
|
||||
public Builder setGid(int gid) {
|
||||
checkArgument(0 <= gid && gid <= 07777777,
|
||||
"Tar gid out of range: %s", gid);
|
||||
setField("gid", 116, 8, String.format("%07o", gid));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file size. (Required)
|
||||
*
|
||||
* <p>This value must be known in advance. There's no such thing as a streaming tar archive.
|
||||
*
|
||||
* @param size must be {@code >=0} and {@code <8^11} which places an eight gigabyte limit.
|
||||
*/
|
||||
public Builder setSize(long size) {
|
||||
checkArgument(0 <= size && size <= 077777777777L,
|
||||
"Tar size out of range: %s", size);
|
||||
setField("size", 124, 12, String.format("%011o", size));
|
||||
hasSize = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the modified time of the file. By default, this is the time the builder object was
|
||||
* constructed.
|
||||
*
|
||||
* <p>The modified time is always stored as a UNIX timestamp which is seconds since the UNIX
|
||||
* epoch in UTC time. Because {@link DateTime} has millisecond precision, it gets rounded down
|
||||
* (floor) to the second.
|
||||
*
|
||||
* @throws NullPointerException
|
||||
*/
|
||||
public Builder setMtime(DateTime mtime) {
|
||||
checkNotNull(mtime, "mtime");
|
||||
setField("mtime", 136, 12, String.format("%011o", mtime.getMillis() / MILLIS_PER_SECOND));
|
||||
return this;
|
||||
}
|
||||
|
||||
private void setChksum() {
|
||||
setField("chksum", 148, 8, String.format("%06o", checksum(header)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file {@link Type}. By default this is {@link Type#REGULAR}.
|
||||
*/
|
||||
public Builder setType(Type type) {
|
||||
switch (type) {
|
||||
case REGULAR:
|
||||
header[156] = '0';
|
||||
break;
|
||||
case DIRECTORY:
|
||||
header[156] = '5';
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UNIX owner of the file. By default this is {@value #DEFAULT_UNAME}.
|
||||
*
|
||||
* @param uname must be {@code <32} characters in length.
|
||||
* @see #setUid(int)
|
||||
* @see #setGname(String)
|
||||
*/
|
||||
public Builder setUname(String uname) {
|
||||
checkArgument(!isNullOrEmpty(uname), "uname");
|
||||
setField("uname", 265, 32, uname);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the UNIX group of the file. By default this is {@value #DEFAULT_GNAME}.
|
||||
*
|
||||
* @param gname must be {@code <32} characters in length.
|
||||
* @see #setGid(int)
|
||||
* @see #setUname(String)
|
||||
*/
|
||||
public Builder setGname(String gname) {
|
||||
checkArgument(!isNullOrEmpty(gname), "gname");
|
||||
setField("gname", 297, 32, gname);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void setMagic() {
|
||||
setField("magic", 257, 6, "ustar");
|
||||
}
|
||||
|
||||
private void setVersion() {
|
||||
header[263] = '0';
|
||||
header[264] = '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new immutable {@link PosixTarHeader} instance.
|
||||
*
|
||||
* <p>It's safe to save a reference to the builder instance and call this method multiple times
|
||||
* because the header data is copied into the resulting object.
|
||||
*
|
||||
* @throws IllegalStateException if you forgot to call required setters.
|
||||
*/
|
||||
public PosixTarHeader build() {
|
||||
checkState(hasName, "name not set");
|
||||
checkState(hasSize, "size not set");
|
||||
hasName = false;
|
||||
hasSize = false;
|
||||
setChksum(); // Calculate the checksum last.
|
||||
return new PosixTarHeader(header.clone());
|
||||
}
|
||||
|
||||
private void setField(String fieldName, int offset, int max, String data) {
|
||||
byte[] bytes = (data + "\0").getBytes(UTF_8);
|
||||
checkArgument(bytes.length <= max,
|
||||
"%s field exceeds max length of %s: %s", fieldName, max - 1, data);
|
||||
System.arraycopy(bytes, 0, header, offset, bytes.length);
|
||||
Arrays.fill(header, offset + bytes.length, offset + max, (byte) 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Utility methods related to preconditions checking. */
|
||||
public class PreconditionsUtils {
|
||||
|
||||
/**
|
||||
* Checks whether the provided reference is null, throws IAE if it is, and returns it if not.
|
||||
*
|
||||
* <p>This method and its overloads are to substitute for checkNotNull() in cases where it's
|
||||
* preferable to throw an IAE instead of an NPE, such as where we want an IAE to indicate that
|
||||
* it's just a bad argument/parameter and reserve NPEs for bugs and unexpected null values.
|
||||
*/
|
||||
public static <T> T checkArgumentNotNull(T reference) {
|
||||
checkArgument(reference != null);
|
||||
return reference;
|
||||
}
|
||||
|
||||
/** Checks whether the provided reference is null, throws IAE if it is, and returns it if not. */
|
||||
public static <T> T checkArgumentNotNull(T reference, @Nullable Object errorMessage) {
|
||||
checkArgument(reference != null, errorMessage);
|
||||
return reference;
|
||||
}
|
||||
|
||||
/** Checks whether the provided reference is null, throws IAE if it is, and returns it if not. */
|
||||
public static <T> T checkArgumentNotNull(
|
||||
T reference, @Nullable String errorMessageTemplate, @Nullable Object... errorMessageArgs) {
|
||||
checkArgument(reference != null, errorMessageTemplate, errorMessageArgs);
|
||||
return reference;
|
||||
}
|
||||
|
||||
/** Checks if the provided Optional is present, returns its value if so, and throws IAE if not. */
|
||||
public static <T> T checkArgumentPresent(Optional<T> reference) {
|
||||
checkArgumentNotNull(reference);
|
||||
checkArgument(reference.isPresent());
|
||||
return reference.get();
|
||||
}
|
||||
|
||||
/** Checks if the provided Optional is present, returns its value if so, and throws IAE if not. */
|
||||
public static <T> T checkArgumentPresent(Optional<T> reference, @Nullable Object errorMessage) {
|
||||
checkArgumentNotNull(reference, errorMessage);
|
||||
checkArgument(reference.isPresent(), errorMessage);
|
||||
return reference.get();
|
||||
}
|
||||
|
||||
/** Checks if the provided Optional is present, returns its value if so, and throws IAE if not. */
|
||||
public static <T> T checkArgumentPresent(
|
||||
Optional<T> reference,
|
||||
@Nullable String errorMessageTemplate,
|
||||
@Nullable Object... errorMessageArgs) {
|
||||
checkArgumentNotNull(reference, errorMessageTemplate, errorMessageArgs);
|
||||
checkArgument(reference.isPresent(), errorMessageTemplate, errorMessageArgs);
|
||||
return reference.get();
|
||||
}
|
||||
}
|
45
util/src/main/java/google/registry/util/PredicateUtils.java
Normal file
45
util/src/main/java/google/registry/util/PredicateUtils.java
Normal file
|
@ -0,0 +1,45 @@
|
|||
// 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.util;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Utility class containing {@link Predicate} methods. */
|
||||
public class PredicateUtils {
|
||||
|
||||
/**
|
||||
* A predicate for a given class X that checks if tested classes are supertypes of X.
|
||||
*
|
||||
* <p>We need our own predicate because Guava's class predicates are backwards.
|
||||
* @see <a href="https://github.com/google/guava/issues/1444">Guava issue #1444</a>
|
||||
*/
|
||||
private static class SupertypeOfPredicate implements Predicate<Class<?>> {
|
||||
|
||||
private final Class<?> subClass;
|
||||
|
||||
SupertypeOfPredicate(Class<?> subClass) {
|
||||
this.subClass = subClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Class<?> superClass) {
|
||||
return superClass.isAssignableFrom(subClass);
|
||||
}
|
||||
}
|
||||
|
||||
public static Predicate<Class<?>> supertypeOf(Class<?> subClass) {
|
||||
return new SupertypeOfPredicate(subClass);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/** Random string generator. */
|
||||
public class RandomStringGenerator extends StringGenerator {
|
||||
|
||||
private final SecureRandom random;
|
||||
|
||||
public RandomStringGenerator(String alphabet, SecureRandom random) {
|
||||
super(alphabet);
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
/** Generates a random string of a specified length. */
|
||||
@Override
|
||||
public String createString(int length) {
|
||||
checkArgument(length > 0);
|
||||
char[] password = new char[length];
|
||||
for (int i = 0; i < length; ++i) {
|
||||
password[i] = alphabet.charAt(random.nextInt(alphabet.length()));
|
||||
}
|
||||
return new String(password);
|
||||
}
|
||||
}
|
40
util/src/main/java/google/registry/util/RegistrarUtils.java
Normal file
40
util/src/main/java/google/registry/util/RegistrarUtils.java
Normal file
|
@ -0,0 +1,40 @@
|
|||
// 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.util;
|
||||
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.CharMatcher;
|
||||
|
||||
/** Utilities for working with {@code Registrar} objects. */
|
||||
public class RegistrarUtils {
|
||||
|
||||
private static final CharMatcher ASCII_LETTER_OR_DIGIT_MATCHER =
|
||||
JavaCharMatchers.asciiLetterOrDigitMatcher();
|
||||
|
||||
/** Strip out anything that isn't a letter or digit, and lowercase. */
|
||||
public static String normalizeRegistrarName(String name) {
|
||||
return Ascii.toLowerCase(ASCII_LETTER_OR_DIGIT_MATCHER.retainFrom(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a normalized registrar clientId by taking the input and making it lowercase and
|
||||
* removing all characters that aren't alphanumeric or hyphens. The normalized id should be unique
|
||||
* in Datastore, and is suitable for use in email addresses.
|
||||
*/
|
||||
public static String normalizeClientId(String clientId) {
|
||||
return Ascii.toLowerCase(clientId).replaceAll("[^a-z0-9\\-]", "");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// 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.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** Used to query whether requests are still running. */
|
||||
public interface RequestStatusChecker extends Serializable {
|
||||
|
||||
/**
|
||||
* Returns the unique log identifier of the current request.
|
||||
*
|
||||
* <p>Multiple calls must return the same value during the same Request.
|
||||
*/
|
||||
String getLogId();
|
||||
|
||||
/**
|
||||
* Returns true if the given request is currently running.
|
||||
*/
|
||||
boolean isRunning(String requestLogId);
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.appengine.api.log.LogQuery;
|
||||
import com.google.appengine.api.log.LogService;
|
||||
import com.google.appengine.api.log.LogServiceFactory;
|
||||
import com.google.appengine.api.log.RequestLogs;
|
||||
import com.google.apphosting.api.ApiProxy;
|
||||
import com.google.apphosting.api.ApiProxy.Environment;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.util.Collections;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Implementation of the {@link RequestStatusChecker} interface. */
|
||||
public class RequestStatusCheckerImpl implements RequestStatusChecker {
|
||||
|
||||
private static final long serialVersionUID = -8161977032130865437L;
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@VisibleForTesting
|
||||
static LogService logService = LogServiceFactory.getLogService();
|
||||
|
||||
/**
|
||||
* The key to {@link Environment#getAttributes}'s request_log_id value.
|
||||
*/
|
||||
private static final String REQUEST_LOG_ID_KEY = "com.google.appengine.runtime.request_log_id";
|
||||
|
||||
@Inject public RequestStatusCheckerImpl() {}
|
||||
|
||||
/**
|
||||
* Returns the unique log identifier of the current request.
|
||||
*
|
||||
* <p>May be safely called multiple times, will always return the same result (within the same
|
||||
* request).
|
||||
*
|
||||
* @see <a href="https://cloud.google.com/appengine/docs/standard/java/how-requests-are-handled#request-ids">appengine documentation</a>
|
||||
*/
|
||||
@Override
|
||||
public String getLogId() {
|
||||
String requestLogId =
|
||||
ApiProxy.getCurrentEnvironment().getAttributes().get(REQUEST_LOG_ID_KEY).toString();
|
||||
logger.atInfo().log("Current requestLogId: %s", requestLogId);
|
||||
// We want to make sure there actually is a log to query for this request, even if the request
|
||||
// dies right after this call.
|
||||
//
|
||||
// flushLogs() is synchronous, so once the function returns, no matter what happens next, the
|
||||
// returned requestLogId will point to existing logs.
|
||||
ApiProxy.flushLogs();
|
||||
return requestLogId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given request is currently running.
|
||||
*
|
||||
* @see <a href="https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/log/LogQuery">appengine documentation</a>
|
||||
*/
|
||||
@Override
|
||||
public boolean isRunning(String requestLogId) {
|
||||
RequestLogs requestLogs =
|
||||
Iterables.getOnlyElement(
|
||||
logService.fetch(
|
||||
LogQuery.Builder
|
||||
.withRequestIds(Collections.singletonList(requestLogId))
|
||||
.includeAppLogs(false)
|
||||
.includeIncomplete(true)),
|
||||
null);
|
||||
// requestLogs will be null if that requestLogId isn't found at all, which can happen if the
|
||||
// request is too new (it can take several seconds until the logs are available for "fetch").
|
||||
// So we have to assume it's "running" in that case.
|
||||
if (requestLogs == null) {
|
||||
logger.atInfo().log(
|
||||
"Queried an unrecognized requestLogId %s - assume it's running", requestLogId);
|
||||
return true;
|
||||
}
|
||||
logger.atInfo().log(
|
||||
"Found logs for requestLogId %s - isFinished: %b", requestLogId, requestLogs.isFinished());
|
||||
return !requestLogs.isFinished();
|
||||
}
|
||||
}
|
55
util/src/main/java/google/registry/util/ResourceUtils.java
Normal file
55
util/src/main/java/google/registry/util/ResourceUtils.java
Normal file
|
@ -0,0 +1,55 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.io.Resources.asByteSource;
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.google.common.io.Resources;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
/** Utility methods related to reading java resources. */
|
||||
public final class ResourceUtils {
|
||||
|
||||
/** Loads a resource from a file as a string, assuming UTF-8 encoding. */
|
||||
public static String readResourceUtf8(String filename) {
|
||||
return readResourceUtf8(getResource(filename));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a resource from a file (specified relative to the contextClass) as a string, assuming
|
||||
* UTF-8 encoding.
|
||||
*/
|
||||
public static String readResourceUtf8(Class<?> contextClass, String filename) {
|
||||
return readResourceUtf8(getResource(contextClass, filename));
|
||||
}
|
||||
|
||||
/** Loads a resource from a URL as a string, assuming UTF-8 encoding. */
|
||||
public static String readResourceUtf8(URL url) {
|
||||
try {
|
||||
return Resources.toString(url, UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to load resource: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads a file (specified relative to the contextClass) as a ByteSource. */
|
||||
public static ByteSource readResourceBytes(Class<?> contextClass, String filename) {
|
||||
return asByteSource(getResource(contextClass, filename));
|
||||
}
|
||||
}
|
176
util/src/main/java/google/registry/util/Retrier.java
Normal file
176
util/src/main/java/google/registry/util/Retrier.java
Normal file
|
@ -0,0 +1,176 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||
import static com.google.common.math.IntMath.pow;
|
||||
import static google.registry.util.PredicateUtils.supertypeOf;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Predicate;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/** Wrapper that does retry with exponential backoff. */
|
||||
public class Retrier implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1167386907195735483L;
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final Sleeper sleeper;
|
||||
private final int attempts;
|
||||
|
||||
/** Holds functions to call whenever the code being retried fails. */
|
||||
public interface FailureReporter {
|
||||
|
||||
/**
|
||||
* Called after a retriable failure happened.
|
||||
*
|
||||
* <p>Not called after the final failure, nor if the Throwable thrown isn't "a retriable error".
|
||||
*
|
||||
* <p>Not called at all if the retrier succeeded on its first attempt.
|
||||
*/
|
||||
void beforeRetry(Throwable thrown, int failures, int maxAttempts);
|
||||
}
|
||||
|
||||
@Inject
|
||||
public Retrier(Sleeper sleeper, @Named("transientFailureRetries") int transientFailureRetries) {
|
||||
this.sleeper = sleeper;
|
||||
checkArgument(transientFailureRetries > 0, "Number of attempts must be positive");
|
||||
this.attempts = transientFailureRetries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a unit of work in the face of transient errors.
|
||||
*
|
||||
* <p>Retrying is done a fixed number of times, with exponential backoff, if the exception that is
|
||||
* thrown is deemed retryable by the predicate. If the error is not considered retryable, or if
|
||||
* the thread is interrupted, or if the allowable number of attempts has been exhausted, the
|
||||
* original exception is propagated through to the caller. Checked exceptions are wrapped in a
|
||||
* RuntimeException, while unchecked exceptions are propagated as-is.
|
||||
*
|
||||
* @return <V> the value returned by the {@link Callable}.
|
||||
*/
|
||||
private <V> V callWithRetry(
|
||||
Callable<V> callable,
|
||||
FailureReporter failureReporter,
|
||||
Predicate<Throwable> isRetryable) {
|
||||
int failures = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return callable.call();
|
||||
} catch (Throwable e) {
|
||||
if (++failures == attempts || !isRetryable.test(e)) {
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
failureReporter.beforeRetry(e, failures, attempts);
|
||||
try {
|
||||
// Wait 100ms on the first attempt, doubling on each subsequent attempt.
|
||||
sleeper.sleep(Duration.millis(pow(2, failures) * 100));
|
||||
} catch (InterruptedException e2) {
|
||||
// Since we're not rethrowing InterruptedException, set the interrupt state on the thread
|
||||
// so the next blocking operation will know to abort the thread.
|
||||
Thread.currentThread().interrupt();
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a unit of work in the face of transient errors and returns the result.
|
||||
*
|
||||
* <p>Retrying is done a fixed number of times, with exponential backoff, if the exception that is
|
||||
* thrown is on a whitelist of retryable errors. If the error is not on the whitelist, or if the
|
||||
* thread is interrupted, or if the allowable number of attempts has been exhausted, the original
|
||||
* exception is propagated through to the caller. Checked exceptions are wrapped in a
|
||||
* RuntimeException, while unchecked exceptions are propagated as-is.
|
||||
*
|
||||
* <p>Uses a default FailureReporter that logs before each retry.
|
||||
*
|
||||
* @return <V> the value returned by the {@link Callable}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final <V> V callWithRetry(
|
||||
Callable<V> callable,
|
||||
Class<? extends Throwable> retryableError,
|
||||
Class<? extends Throwable>... moreRetryableErrors) {
|
||||
return callWithRetry(callable, LOGGING_FAILURE_REPORTER, retryableError, moreRetryableErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a unit of work in the face of transient errors, without returning a value.
|
||||
*
|
||||
* @see #callWithRetry(Callable, Class, Class[])
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final void callWithRetry(
|
||||
VoidCallable callable,
|
||||
Class<? extends Throwable> retryableError,
|
||||
Class<? extends Throwable>... moreRetryableErrors) {
|
||||
callWithRetry(callable.asCallable(), retryableError, moreRetryableErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a unit of work in the face of transient errors and returns the result.
|
||||
*
|
||||
* <p>Retrying is done a fixed number of times, with exponential backoff, if the exception that is
|
||||
* thrown is on a whitelist of retryable errors. If the error is not on the whitelist, or if the
|
||||
* thread is interrupted, or if the allowable number of attempts has been exhausted, the original
|
||||
* exception is propagated through to the caller. Checked exceptions are wrapped in a
|
||||
* RuntimeException, while unchecked exceptions are propagated as-is.
|
||||
*
|
||||
* @return <V> the value returned by the {@link Callable}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final <V> V callWithRetry(
|
||||
Callable<V> callable,
|
||||
FailureReporter failureReporter,
|
||||
Class<? extends Throwable> retryableError,
|
||||
Class<? extends Throwable>... moreRetryableErrors) {
|
||||
final Set<Class<?>> retryables =
|
||||
new ImmutableSet.Builder<Class<?>>().add(retryableError).add(moreRetryableErrors).build();
|
||||
return callWithRetry(
|
||||
callable, failureReporter, e -> retryables.stream().anyMatch(supertypeOf(e.getClass())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a unit of work in the face of transient errors, without returning a value.
|
||||
*
|
||||
* @see #callWithRetry(Callable, FailureReporter, Class, Class[])
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final void callWithRetry(
|
||||
VoidCallable callable,
|
||||
FailureReporter failureReporter,
|
||||
Class<? extends Throwable> retryableError,
|
||||
Class<? extends Throwable>... moreRetryableErrors) {
|
||||
callWithRetry(callable.asCallable(), failureReporter, retryableError, moreRetryableErrors);
|
||||
}
|
||||
|
||||
private static final FailureReporter LOGGING_FAILURE_REPORTER =
|
||||
(thrown, failures, maxAttempts) ->
|
||||
logger.atInfo().withCause(thrown).log(
|
||||
"Retrying transient error, attempt %d/%d", failures, maxAttempts);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.collect.Iterables.toArray;
|
||||
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.util.EmailMessage.Attachment;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.mail.BodyPart;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.Message.RecipientType;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Multipart;
|
||||
import javax.mail.Session;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeBodyPart;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
|
||||
/**
|
||||
* Wrapper around javax.mail's email creation and sending functionality. Encompasses a retry policy
|
||||
* as well.
|
||||
*/
|
||||
@Singleton
|
||||
public class SendEmailService {
|
||||
|
||||
private final Retrier retrier;
|
||||
private final TransportEmailSender transportEmailSender;
|
||||
|
||||
@Inject
|
||||
SendEmailService(Retrier retrier, TransportEmailSender transportEmailSender) {
|
||||
this.retrier = retrier;
|
||||
this.transportEmailSender = transportEmailSender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided message content into a {@link javax.mail.Message} and sends it with
|
||||
* retry on transient failures.
|
||||
*/
|
||||
public void sendEmail(EmailMessage emailMessage) {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
Message msg =
|
||||
new MimeMessage(
|
||||
Session.getDefaultInstance(new Properties(), /* authenticator= */ null));
|
||||
msg.setFrom(emailMessage.from());
|
||||
msg.addRecipients(
|
||||
RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class));
|
||||
msg.setSubject(emailMessage.subject());
|
||||
|
||||
Multipart multipart = new MimeMultipart();
|
||||
BodyPart bodyPart = new MimeBodyPart();
|
||||
bodyPart.setContent(
|
||||
emailMessage.body(),
|
||||
emailMessage.contentType().orElse(MediaType.PLAIN_TEXT_UTF_8).toString());
|
||||
multipart.addBodyPart(bodyPart);
|
||||
|
||||
if (emailMessage.attachment().isPresent()) {
|
||||
Attachment attachment = emailMessage.attachment().get();
|
||||
BodyPart attachmentPart = new MimeBodyPart();
|
||||
attachmentPart.setContent(attachment.content(), attachment.contentType().toString());
|
||||
attachmentPart.setFileName(attachment.filename());
|
||||
multipart.addBodyPart(attachmentPart);
|
||||
}
|
||||
if (emailMessage.bcc().isPresent()) {
|
||||
msg.addRecipient(RecipientType.BCC, emailMessage.bcc().get());
|
||||
}
|
||||
msg.setContent(multipart);
|
||||
msg.saveChanges();
|
||||
transportEmailSender.sendMessage(msg);
|
||||
},
|
||||
IOException.class,
|
||||
MessagingException.class);
|
||||
}
|
||||
}
|
69
util/src/main/java/google/registry/util/SerializeUtils.java
Normal file
69
util/src/main/java/google/registry/util/SerializeUtils.java
Normal file
|
@ -0,0 +1,69 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Utilities for easy serialization with informative error messages. */
|
||||
public final class SerializeUtils {
|
||||
|
||||
/**
|
||||
* Turns an object into a byte array.
|
||||
*
|
||||
* @return serialized object or {@code null} if {@code value} is {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static byte[] serialize(@Nullable Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream objectBytes = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(objectBytes)) {
|
||||
oos.writeObject(value);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Unable to serialize: " + value, e);
|
||||
}
|
||||
return objectBytes.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a byte array into an object.
|
||||
*
|
||||
* @return deserialized object or {@code null} if {@code objectBytes} is {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> T deserialize(Class<T> type, @Nullable byte[] objectBytes) {
|
||||
checkNotNull(type);
|
||||
if (objectBytes == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return type.cast(new ObjectInputStream(new ByteArrayInputStream(objectBytes)).readObject());
|
||||
} catch (ClassNotFoundException | IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to deserialize: objectBytes=" + base16().encode(objectBytes), e);
|
||||
}
|
||||
}
|
||||
|
||||
private SerializeUtils() {}
|
||||
}
|
45
util/src/main/java/google/registry/util/Sleeper.java
Normal file
45
util/src/main/java/google/registry/util/Sleeper.java
Normal file
|
@ -0,0 +1,45 @@
|
|||
// 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.util;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import org.joda.time.ReadableDuration;
|
||||
|
||||
/**
|
||||
* An object which accepts requests to put the current thread to sleep.
|
||||
*
|
||||
* @see SystemSleeper
|
||||
* @see google.registry.testing.FakeSleeper
|
||||
*/
|
||||
@ThreadSafe
|
||||
public interface Sleeper {
|
||||
|
||||
/**
|
||||
* Puts the current thread to sleep.
|
||||
*
|
||||
* @throws InterruptedException if this thread was interrupted
|
||||
*/
|
||||
void sleep(ReadableDuration duration) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Puts the current thread to sleep, ignoring interrupts.
|
||||
*
|
||||
* <p>If {@link InterruptedException} was caught, then {@code Thread.currentThread().interrupt()}
|
||||
* will be called at the end of the {@code duration}.
|
||||
*
|
||||
* @see com.google.common.util.concurrent.Uninterruptibles#sleepUninterruptibly
|
||||
*/
|
||||
void sleepUninterruptibly(ReadableDuration duration);
|
||||
}
|
103
util/src/main/java/google/registry/util/SqlTemplate.java
Normal file
103
util/src/main/java/google/registry/util/SqlTemplate.java
Normal file
|
@ -0,0 +1,103 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.re2j.Matcher;
|
||||
import com.google.re2j.Pattern;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
/** SQL template variable substitution. */
|
||||
@Immutable
|
||||
public final class SqlTemplate {
|
||||
|
||||
private static final Pattern KEY_PATTERN = Pattern.compile("[A-Z][_A-Z0-9]*");
|
||||
|
||||
private static final Pattern SEARCH_PATTERN =
|
||||
Pattern.compile("(['\"]?)%(" + KEY_PATTERN + ")%(['\"]?)");
|
||||
|
||||
private static final CharMatcher LEGAL_SUBSTITUTIONS =
|
||||
JavaCharMatchers.asciiLetterOrDigitMatcher().or(CharMatcher.anyOf("-_.,: "));
|
||||
|
||||
/** Returns a new immutable SQL template builder object, for query parameter substitution. */
|
||||
public static SqlTemplate create(String template) {
|
||||
return new SqlTemplate(template, ImmutableMap.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key/value that should be substituted an individual variable in the template.
|
||||
*
|
||||
* <p>Your template variables should appear as follows: {@code WHERE foo = '%BAR%'} and you
|
||||
* would call {@code .put("BAR", "some value"} to safely substitute it with a value. Only
|
||||
* whitelisted characters (as defined by {@link #LEGAL_SUBSTITUTIONS}) are allowed in values.
|
||||
*
|
||||
* @param key uppercase string that can have digits and underscores
|
||||
* @param value substitution value, comprised of whitelisted characters
|
||||
* @throws IllegalArgumentException if key or value has bad chars or duplicate keys were added
|
||||
*/
|
||||
public SqlTemplate put(String key, String value) {
|
||||
checkArgument(KEY_PATTERN.matcher(key).matches(), "Bad substitution key: %s", key);
|
||||
checkArgument(LEGAL_SUBSTITUTIONS.matchesAllOf(value), "Illegal characters in %s", value);
|
||||
return new SqlTemplate(template, new ImmutableMap.Builder<String, String>()
|
||||
.putAll(substitutions)
|
||||
.put(key, value)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the freshly substituted SQL code.
|
||||
*
|
||||
* @throws IllegalArgumentException if any substitution variable is not found in the template,
|
||||
* or if there are any variable-like strings (%something%) left after substitution.
|
||||
*/
|
||||
public String build() {
|
||||
StringBuffer result = new StringBuffer(template.length());
|
||||
Set<String> found = new HashSet<>();
|
||||
Matcher matcher = SEARCH_PATTERN.matcher(template);
|
||||
while (matcher.find()) {
|
||||
String wholeMatch = matcher.group(0);
|
||||
String leftQuote = matcher.group(1);
|
||||
String key = matcher.group(2);
|
||||
String rightQuote = matcher.group(3);
|
||||
String value = substitutions.get(key);
|
||||
checkArgumentNotNull(value, "%%s% found in template but no substitution specified", key);
|
||||
checkArgument(leftQuote.equals(rightQuote), "Quote mismatch: %s", wholeMatch);
|
||||
matcher.appendReplacement(result, String.format("%s%s%s", leftQuote, value, rightQuote));
|
||||
found.add(key);
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
Set<String> remaining = difference(substitutions.keySet(), found);
|
||||
checkArgument(remaining.isEmpty(),
|
||||
"Not found in template: %s", Joiner.on(", ").join(remaining));
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private final String template;
|
||||
private final ImmutableMap<String, String> substitutions;
|
||||
|
||||
private SqlTemplate(String template, ImmutableMap<String, String> substitutions) {
|
||||
this.template = checkNotNull(template);
|
||||
this.substitutions = checkNotNull(substitutions);
|
||||
}
|
||||
}
|
62
util/src/main/java/google/registry/util/StringGenerator.java
Normal file
62
util/src/main/java/google/registry/util/StringGenerator.java
Normal file
|
@ -0,0 +1,62 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
|
||||
/** String generator. */
|
||||
public abstract class StringGenerator implements Serializable {
|
||||
|
||||
public static final int DEFAULT_PASSWORD_LENGTH = 16;
|
||||
|
||||
/** A class containing different alphabets used to generate strings. */
|
||||
public static class Alphabets {
|
||||
|
||||
/** A URL-safe Base64 alphabet (alphanumeric, hyphen, underscore). */
|
||||
public static final String BASE_64 =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
|
||||
|
||||
/** An alphanumeric alphabet that omits visually similar characters. */
|
||||
public static final String BASE_58 =
|
||||
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
/** Digit-only alphabet. */
|
||||
public static final String DIGITS_ONLY = "0123456789";
|
||||
}
|
||||
|
||||
protected String alphabet;
|
||||
|
||||
protected StringGenerator(String alphabet) {
|
||||
checkArgument(!isNullOrEmpty(alphabet), "Alphabet cannot be null or empty.");
|
||||
this.alphabet = alphabet;
|
||||
}
|
||||
|
||||
/** Generates a string of a specified length. */
|
||||
public abstract String createString(int length);
|
||||
|
||||
/** Batch-generates an {@link ImmutableList} of strings of a specified length. */
|
||||
public Collection<String> createStrings(int length, int count) {
|
||||
ImmutableList.Builder<String> listBuilder = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
listBuilder.add(createString(length));
|
||||
}
|
||||
return listBuilder.build();
|
||||
}
|
||||
}
|
37
util/src/main/java/google/registry/util/SystemClock.java
Normal file
37
util/src/main/java/google/registry/util/SystemClock.java
Normal file
|
@ -0,0 +1,37 @@
|
|||
// 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.util;
|
||||
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Clock implementation that proxies to the real system clock. */
|
||||
@ThreadSafe
|
||||
public class SystemClock implements Clock {
|
||||
|
||||
private static final long serialVersionUID = 5165372013848947515L;
|
||||
|
||||
@Inject
|
||||
public SystemClock() {}
|
||||
|
||||
/** Returns the current time. */
|
||||
@Override
|
||||
public DateTime nowUtc() {
|
||||
return DateTime.now(UTC);
|
||||
}
|
||||
}
|
46
util/src/main/java/google/registry/util/SystemSleeper.java
Normal file
46
util/src/main/java/google/registry/util/SystemSleeper.java
Normal file
|
@ -0,0 +1,46 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
import java.io.Serializable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.ReadableDuration;
|
||||
|
||||
/** Implementation of {@link Sleeper} for production use. */
|
||||
@ThreadSafe
|
||||
public final class SystemSleeper implements Sleeper, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 2003215961965322843L;
|
||||
|
||||
@Inject
|
||||
public SystemSleeper() {}
|
||||
|
||||
@Override
|
||||
public void sleep(ReadableDuration duration) throws InterruptedException {
|
||||
checkArgument(duration.getMillis() >= 0);
|
||||
Thread.sleep(duration.getMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sleepUninterruptibly(ReadableDuration duration) {
|
||||
checkArgument(duration.getMillis() >= 0);
|
||||
Uninterruptibles.sleepUninterruptibly(duration.getMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
101
util/src/main/java/google/registry/util/TaskQueueUtils.java
Normal file
101
util/src/main/java/google/registry/util/TaskQueueUtils.java
Normal file
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2018 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.util;
|
||||
|
||||
import com.google.appengine.api.taskqueue.Queue;
|
||||
import com.google.appengine.api.taskqueue.TaskHandle;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.appengine.api.taskqueue.TransientFailureException;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Utilities for dealing with App Engine task queues. */
|
||||
public class TaskQueueUtils implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 7893211200220508362L;
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
public TaskQueueUtils(Retrier retrier) {
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
@NonFinalForTesting
|
||||
@VisibleForTesting
|
||||
static int BATCH_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* The batch size to use for App Engine task queue operations.
|
||||
*
|
||||
* <p>Note that 1,000 is currently the maximum allowable batch size in App Engine.
|
||||
*/
|
||||
public static int getBatchSize() {
|
||||
return BATCH_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task to a App Engine task queue in a reliable manner.
|
||||
*
|
||||
* <p>This is the same as {@link Queue#add(TaskOptions)} except it'll automatically retry with
|
||||
* exponential backoff if {@link TransientFailureException} is thrown.
|
||||
*
|
||||
* @throws TransientFailureException if retrying failed for the maximum period of time, or an
|
||||
* {@link InterruptedException} told us to stop trying
|
||||
* @return successfully enqueued task
|
||||
*/
|
||||
public TaskHandle enqueue(Queue queue, TaskOptions task) {
|
||||
return enqueue(queue, ImmutableList.of(task)).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds tasks to an App Engine task queue in a reliable manner.
|
||||
*
|
||||
* <p>This is the same as {@link Queue#add(Iterable)} except it'll automatically retry with
|
||||
* exponential backoff if {@link TransientFailureException} is thrown.
|
||||
*
|
||||
* @throws TransientFailureException if retrying failed for the maximum period of time, or an
|
||||
* {@link InterruptedException} told us to stop trying
|
||||
* @return successfully enqueued tasks
|
||||
*/
|
||||
public List<TaskHandle> enqueue(final Queue queue, final Iterable<TaskOptions> tasks) {
|
||||
return retrier.callWithRetry(
|
||||
() -> {
|
||||
for (TaskOptions task : tasks) {
|
||||
logger.atInfo().log(
|
||||
"Enqueuing queue='%s' endpoint='%s'", queue.getQueueName(), task.getUrl());
|
||||
}
|
||||
return queue.add(tasks);
|
||||
},
|
||||
TransientFailureException.class);
|
||||
}
|
||||
|
||||
/** Deletes the specified tasks from the queue in batches, with retrying. */
|
||||
public void deleteTasks(Queue queue, List<TaskHandle> tasks) {
|
||||
Lists.partition(tasks, BATCH_SIZE)
|
||||
.stream()
|
||||
.forEach(
|
||||
batch ->
|
||||
retrier.callWithRetry(
|
||||
() -> queue.deleteTask(batch), TransientFailureException.class));
|
||||
}
|
||||
}
|
67
util/src/main/java/google/registry/util/TeeOutputStream.java
Normal file
67
util/src/main/java/google/registry/util/TeeOutputStream.java
Normal file
|
@ -0,0 +1,67 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import javax.annotation.WillNotClose;
|
||||
|
||||
/**
|
||||
* {@link OutputStream} delegate that writes simultaneously to multiple other output streams.
|
||||
*/
|
||||
public final class TeeOutputStream extends OutputStream {
|
||||
|
||||
private final ImmutableList<? extends OutputStream> outputs;
|
||||
private boolean isClosed;
|
||||
|
||||
public TeeOutputStream(@WillNotClose Iterable<? extends OutputStream> outputs) {
|
||||
this.outputs = ImmutableList.copyOf(outputs);
|
||||
checkArgument(!this.outputs.isEmpty(), "must provide at least one output stream");
|
||||
}
|
||||
|
||||
/** @see java.io.OutputStream#write(int) */
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
checkState(!isClosed, "outputstream closed");
|
||||
for (OutputStream out : outputs) {
|
||||
out.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
/** @see #write(byte[], int, int) */
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
this.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
/** @see java.io.OutputStream#write(byte[], int, int) */
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
checkState(!isClosed, "outputstream closed");
|
||||
for (OutputStream out : outputs) {
|
||||
out.write(b, off, len);
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the stream. Any calls to a {@code write()} method after this will throw. */
|
||||
@Override
|
||||
public void close() {
|
||||
isClosed = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// 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.util;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Transport;
|
||||
|
||||
/** Wrapper for sending email so that we can test {@link google.registry.util.SendEmailService}. */
|
||||
@Singleton
|
||||
class TransportEmailSender {
|
||||
|
||||
@Inject
|
||||
TransportEmailSender() {}
|
||||
|
||||
/** Sends a message using default App Engine transport. */
|
||||
void sendMessage(Message msg) throws MessagingException {
|
||||
Transport.send(msg);
|
||||
}
|
||||
}
|
136
util/src/main/java/google/registry/util/TypeUtils.java
Normal file
136
util/src/main/java/google/registry/util/TypeUtils.java
Normal file
|
@ -0,0 +1,136 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.CollectionUtils.difference;
|
||||
import static java.lang.reflect.Modifier.isFinal;
|
||||
import static java.lang.reflect.Modifier.isStatic;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Utilities methods related to reflection. */
|
||||
public class TypeUtils {
|
||||
|
||||
/** A {@TypeToken} that removes an ugly cast in the common cases of getting a known type. */
|
||||
public static class TypeInstantiator<T> extends TypeToken<T> {
|
||||
protected TypeInstantiator(Class<?> declaringClass) {
|
||||
super(declaringClass);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Class<T> getExactType() {
|
||||
return (Class<T>) getRawType();
|
||||
}
|
||||
|
||||
public T instantiate() {
|
||||
return TypeUtils.instantiate(getExactType());
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T instantiate(Class<? extends T> clazz) {
|
||||
checkArgument(Modifier.isPublic(clazz.getModifiers()),
|
||||
"AppEngine's custom security manager won't let us reflectively access non-public types");
|
||||
try {
|
||||
return clazz.getConstructor().newInstance();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a class with the specified constructor argument.
|
||||
*
|
||||
* <p>Because we use arg1's type to lookup the constructor, this only works if arg1's class is
|
||||
* exactly the same type as the constructor argument. Subtypes are not allowed.
|
||||
*/
|
||||
public static <T, U> T instantiate(Class<? extends T> clazz, U arg1) {
|
||||
checkArgument(Modifier.isPublic(clazz.getModifiers()),
|
||||
"AppEngine's custom security manager won't let us reflectively access non-public types");
|
||||
try {
|
||||
return clazz.getConstructor(arg1.getClass()).newInstance(arg1);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class referred to by a fully qualified class name string.
|
||||
*
|
||||
* <p>Throws an error if the loaded class is not assignable from the expected super type class.
|
||||
*/
|
||||
public static <T> Class<T> getClassFromString(String className, Class<T> expectedSuperType) {
|
||||
Class<?> clazz;
|
||||
try {
|
||||
clazz = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IllegalArgumentException(String.format("Failed to load class %s", className), e);
|
||||
}
|
||||
checkArgument(
|
||||
expectedSuperType.isAssignableFrom(clazz),
|
||||
"%s does not implement/extend %s",
|
||||
clazz.getSimpleName(),
|
||||
expectedSuperType.getSimpleName());
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<T> castedClass = (Class<T>) clazz;
|
||||
return castedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates enum "values" in a typesafe enum pattern into a string->field map.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> ImmutableMap<String, T> getTypesafeEnumMapping(Class<T> clazz) {
|
||||
ImmutableMap.Builder<String, T> builder = new ImmutableMap.Builder<>();
|
||||
for (Field field : clazz.getFields()) {
|
||||
if (isFinal(field.getModifiers())
|
||||
&& isStatic(field.getModifiers())
|
||||
&& clazz.isAssignableFrom(field.getType())) {
|
||||
try {
|
||||
T enumField = (T) field.get(null);
|
||||
builder.put(field.getName(), enumField);
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
throw new RuntimeException(String.format(
|
||||
"Could not retrieve static final field mapping for %s", clazz.getName()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Returns a predicate that tests whether classes are annotated with the given annotation. */
|
||||
public static Predicate<Class<?>> hasAnnotation(
|
||||
final Class<? extends Annotation> annotation) {
|
||||
return clazz -> clazz.isAnnotationPresent(annotation);
|
||||
}
|
||||
|
||||
public static void checkNoInheritanceRelationships(ImmutableSet<Class<?>> resourceClasses) {
|
||||
for (Class<?> resourceClass : resourceClasses) {
|
||||
for (Class<?> potentialSuperclass : difference(resourceClasses, resourceClass)) {
|
||||
checkArgument(
|
||||
!potentialSuperclass.isAssignableFrom(resourceClass),
|
||||
"Cannot specify resource classes with inheritance relationship: %s extends %s",
|
||||
resourceClass,
|
||||
potentialSuperclass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
util/src/main/java/google/registry/util/UrlChecker.java
Normal file
70
util/src/main/java/google/registry/util/UrlChecker.java
Normal file
|
@ -0,0 +1,70 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Throwables.throwIfUnchecked;
|
||||
import static java.util.concurrent.Executors.newCachedThreadPool;
|
||||
|
||||
import com.google.common.util.concurrent.SimpleTimeLimiter;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** An utility to probe a given url until it becomes available. */
|
||||
public final class UrlChecker {
|
||||
|
||||
private static final int READ_TIMEOUT_MS = 1000;
|
||||
private static final int CONNECT_TIMEOUT_MS = 500;
|
||||
|
||||
/** Probes {@code url} until it becomes available. */
|
||||
public static void waitUntilAvailable(final URL url, int timeoutMs) {
|
||||
try {
|
||||
Void unusedReturnValue = SimpleTimeLimiter.create(newCachedThreadPool())
|
||||
.callWithTimeout(
|
||||
() -> {
|
||||
int exponentialBackoffMs = 1;
|
||||
while (true) {
|
||||
if (isAvailable(url)) {
|
||||
return null;
|
||||
}
|
||||
Thread.sleep(exponentialBackoffMs *= 2);
|
||||
}
|
||||
},
|
||||
timeoutMs,
|
||||
TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns {@code true} if page is available and returns {@code 200 OK}. */
|
||||
static boolean isAvailable(URL url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
try {
|
||||
connection.connect();
|
||||
return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private UrlChecker() {}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.appengine.api.urlfetch.HTTPHeader;
|
||||
import com.google.appengine.api.urlfetch.HTTPRequest;
|
||||
import com.google.appengine.api.urlfetch.HTTPResponse;
|
||||
|
||||
/**
|
||||
* Exception for when App Engine HTTP requests return a bad response.
|
||||
*
|
||||
* <p>This class displays lots of helpful troubleshooting information.
|
||||
*/
|
||||
public class UrlFetchException extends RuntimeException {
|
||||
|
||||
private final HTTPRequest req;
|
||||
private final HTTPResponse rsp;
|
||||
|
||||
public UrlFetchException(String message, HTTPRequest req, HTTPResponse rsp) {
|
||||
super(message);
|
||||
this.req = checkNotNull(req, "req");
|
||||
this.rsp = checkNotNull(rsp, "rsp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
StringBuilder res =
|
||||
new StringBuilder(2048 + rsp.getContent().length)
|
||||
.append(
|
||||
String.format(
|
||||
"%s: %s (HTTP Status %d)\nX-Fetch-URL: %s\nX-Final-URL: %s\n",
|
||||
getClass().getSimpleName(),
|
||||
super.getMessage(),
|
||||
rsp.getResponseCode(),
|
||||
req.getURL().toString(),
|
||||
rsp.getFinalUrl()));
|
||||
for (HTTPHeader header : rsp.getHeadersUncombined()) {
|
||||
res.append(header.getName());
|
||||
res.append(": ");
|
||||
res.append(header.getValue());
|
||||
res.append('\n');
|
||||
}
|
||||
res.append(">>>\n");
|
||||
res.append(new String(rsp.getContent(), UTF_8));
|
||||
res.append("\n<<<");
|
||||
return res.toString();
|
||||
}
|
||||
}
|
109
util/src/main/java/google/registry/util/UrlFetchUtils.java
Normal file
109
util/src/main/java/google/registry/util/UrlFetchUtils.java
Normal file
|
@ -0,0 +1,109 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
|
||||
import static com.google.common.net.HttpHeaders.CONTENT_DISPOSITION;
|
||||
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
|
||||
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.appengine.api.urlfetch.HTTPHeader;
|
||||
import com.google.appengine.api.urlfetch.HTTPRequest;
|
||||
import com.google.appengine.api.urlfetch.HTTPResponse;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.net.MediaType;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
/** Helper methods for the App Engine URL fetch service. */
|
||||
public final class UrlFetchUtils {
|
||||
|
||||
/** Returns value of first header matching {@code name}. */
|
||||
public static Optional<String> getHeaderFirst(HTTPResponse rsp, String name) {
|
||||
return getHeaderFirstInternal(rsp.getHeadersUncombined(), name);
|
||||
}
|
||||
|
||||
/** Returns value of first header matching {@code name}. */
|
||||
public static Optional<String> getHeaderFirst(HTTPRequest req, String name) {
|
||||
return getHeaderFirstInternal(req.getHeaders(), name);
|
||||
}
|
||||
|
||||
private static Optional<String> getHeaderFirstInternal(Iterable<HTTPHeader> hdrs, String name) {
|
||||
name = Ascii.toLowerCase(name);
|
||||
for (HTTPHeader header : hdrs) {
|
||||
if (Ascii.toLowerCase(header.getName()).equals(name)) {
|
||||
return Optional.of(header.getValue());
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets payload on request as a {@code multipart/form-data} request.
|
||||
*
|
||||
* <p>This is equivalent to running the command: {@code curl -F fieldName=@payload.txt URL}
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2388.txt">RFC2388 - Returning Values from Forms</a>
|
||||
*/
|
||||
public static void setPayloadMultipart(
|
||||
HTTPRequest request,
|
||||
String name,
|
||||
String filename,
|
||||
MediaType contentType,
|
||||
String data,
|
||||
Random random) {
|
||||
String boundary = createMultipartBoundary(random);
|
||||
checkState(
|
||||
!data.contains(boundary),
|
||||
"Multipart data contains autogenerated boundary: %s", boundary);
|
||||
String multipart =
|
||||
String.format("--%s\r\n", boundary)
|
||||
+ String.format(
|
||||
"%s: form-data; name=\"%s\"; filename=\"%s\"\r\n",
|
||||
CONTENT_DISPOSITION, name, filename)
|
||||
+ String.format("%s: %s\r\n", CONTENT_TYPE, contentType)
|
||||
+ "\r\n"
|
||||
+ data
|
||||
+ "\r\n"
|
||||
+ String.format("--%s--\r\n", boundary);
|
||||
byte[] payload = multipart.getBytes(UTF_8);
|
||||
request.addHeader(
|
||||
new HTTPHeader(
|
||||
CONTENT_TYPE, String.format("multipart/form-data;" + " boundary=\"%s\"", boundary)));
|
||||
request.addHeader(new HTTPHeader(CONTENT_LENGTH, Integer.toString(payload.length)));
|
||||
request.setPayload(payload);
|
||||
}
|
||||
|
||||
private static String createMultipartBoundary(Random random) {
|
||||
// Generate 192 random bits (24 bytes) to produce 192/log_2(64) = 192/6 = 32 base64 digits.
|
||||
byte[] rand = new byte[24];
|
||||
random.nextBytes(rand);
|
||||
// Boundary strings can be up to 70 characters long, so use 30 hyphens plus 32 random digits.
|
||||
// See https://tools.ietf.org/html/rfc2046#section-5.1.1
|
||||
return Strings.repeat("-", 30) + base64().encode(rand);
|
||||
}
|
||||
|
||||
/** Sets the HTTP Basic Authentication header on an {@link HTTPRequest}. */
|
||||
public static void setAuthorizationHeader(HTTPRequest req, Optional<String> login) {
|
||||
if (login.isPresent()) {
|
||||
String token = base64().encode(login.get().getBytes(UTF_8));
|
||||
req.addHeader(new HTTPHeader(AUTHORIZATION, "Basic " + token));
|
||||
}
|
||||
}
|
||||
}
|
86
util/src/main/java/google/registry/util/UtilsModule.java
Normal file
86
util/src/main/java/google/registry/util/UtilsModule.java
Normal file
|
@ -0,0 +1,86 @@
|
|||
// 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.util;
|
||||
|
||||
import com.google.appengine.api.modules.ModulesService;
|
||||
import com.google.appengine.api.modules.ModulesServiceFactory;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/** Dagger module to provide instances of various utils classes. */
|
||||
@Module
|
||||
public abstract class UtilsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract Sleeper provideSleeper(SystemSleeper sleeper);
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract Clock provideClock(SystemClock clock);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static ModulesService provideModulesService() {
|
||||
return ModulesServiceFactory.getModulesService();
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract AppEngineServiceUtils provideAppEngineServiceUtils(
|
||||
AppEngineServiceUtilsImpl appEngineServiceUtilsImpl);
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
public static SecureRandom provideSecureRandom() {
|
||||
try {
|
||||
return SecureRandom.getInstance("NativePRNG");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ProviderException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract Random provideSecureRandomAsRandom(SecureRandom random);
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("base58StringGenerator")
|
||||
public static StringGenerator provideBase58StringGenerator(SecureRandom secureRandom) {
|
||||
return new RandomStringGenerator(StringGenerator.Alphabets.BASE_58, secureRandom);
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("base64StringGenerator")
|
||||
public static StringGenerator provideBase64StringGenerator(SecureRandom secureRandom) {
|
||||
return new RandomStringGenerator(StringGenerator.Alphabets.BASE_64, secureRandom);
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("digitOnlyStringGenerator")
|
||||
public static StringGenerator provideDigitsOnlyStringGenerator(SecureRandom secureRandom) {
|
||||
return new RandomStringGenerator(StringGenerator.Alphabets.DIGITS_ONLY, secureRandom);
|
||||
}
|
||||
}
|
34
util/src/main/java/google/registry/util/VoidCallable.java
Normal file
34
util/src/main/java/google/registry/util/VoidCallable.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
// 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.util;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* A functional interface for a version of {@link Callable} that returns no value.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface VoidCallable {
|
||||
|
||||
void call() throws Exception;
|
||||
|
||||
/** Returns the VoidCallable as a {@link Callable} that returns null. */
|
||||
default Callable<Void> asCallable() {
|
||||
return () -> {
|
||||
call();
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
181
util/src/main/java/google/registry/util/X509Utils.java
Normal file
181
util/src/main/java/google/registry/util/X509Utils.java
Normal file
|
@ -0,0 +1,181 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Throwables.throwIfInstanceOf;
|
||||
import static com.google.common.collect.MoreCollectors.onlyElement;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CRLException;
|
||||
import java.security.cert.CRLReason;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.CertificateRevokedException;
|
||||
import java.security.cert.X509CRL;
|
||||
import java.security.cert.X509CRLEntry;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Tainted;
|
||||
|
||||
/** X.509 Public Key Infrastructure (PKI) helper functions. */
|
||||
public final class X509Utils {
|
||||
|
||||
/**
|
||||
* Parse the encoded certificate and return a base64 encoded string (without padding) of the
|
||||
* SHA-256 digest of the certificate.
|
||||
*
|
||||
* <p>Note that this must match the method used by the GFE to generate the client certificate hash
|
||||
* so that the two will match when we check against the whitelist.
|
||||
*/
|
||||
public static String getCertificateHash(X509Certificate cert) {
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
messageDigest.update(cert.getEncoded());
|
||||
return base64().omitPadding().encode(messageDigest.digest());
|
||||
} catch (CertificateException | NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an ASCII-armored public X.509 certificate.
|
||||
*
|
||||
* @throws CertificateParsingException on parsing errors.
|
||||
*/
|
||||
public static X509Certificate loadCertificate(InputStream input)
|
||||
throws CertificateParsingException {
|
||||
try {
|
||||
return CertificateFactory.getInstance("X.509")
|
||||
.generateCertificates(input)
|
||||
.stream()
|
||||
.filter(X509Certificate.class::isInstance)
|
||||
.map(X509Certificate.class::cast)
|
||||
.collect(onlyElement());
|
||||
} catch (CertificateException e) { // CertificateParsingException by specification.
|
||||
throwIfInstanceOf(e, CertificateParsingException.class);
|
||||
throw new CertificateParsingException(e);
|
||||
} catch (NoSuchElementException e) {
|
||||
throw new CertificateParsingException("No X509Certificate found.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new CertificateParsingException("Multiple X509Certificate found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an ASCII-armored public X.509 certificate.
|
||||
*
|
||||
* @throws CertificateParsingException on parsing errors
|
||||
*/
|
||||
public static X509Certificate loadCertificate(String asciiCrt)
|
||||
throws CertificateParsingException {
|
||||
return loadCertificate(new ByteArrayInputStream(asciiCrt.getBytes(US_ASCII)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an ASCII-armored public X.509 certificate.
|
||||
*
|
||||
* @throws CertificateParsingException on parsing errors
|
||||
* @throws IOException on file system errors
|
||||
*/
|
||||
public static X509Certificate loadCertificate(Path certPath)
|
||||
throws CertificateParsingException, IOException {
|
||||
return loadCertificate(Files.newInputStream(certPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an ASCII-armored X.509 certificate revocation list (CRL).
|
||||
*
|
||||
* @throws CRLException on parsing errors.
|
||||
*/
|
||||
public static X509CRL loadCrl(String asciiCrl) throws GeneralSecurityException {
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(asciiCrl.getBytes(US_ASCII));
|
||||
try {
|
||||
return CertificateFactory.getInstance("X.509")
|
||||
.generateCRLs(input)
|
||||
.stream()
|
||||
.filter(X509CRL.class::isInstance)
|
||||
.map(X509CRL.class::cast)
|
||||
.collect(onlyElement());
|
||||
} catch (NoSuchElementException e) {
|
||||
throw new CRLException("No X509CRL found.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new CRLException("Multiple X509CRL found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that {@code cert} is signed by the {@code ca} and not revoked.
|
||||
*
|
||||
* <p>Support for certificate chains has not been implemented.
|
||||
*
|
||||
* @throws GeneralSecurityException for unsupported protocols, certs not signed by the TMCH,
|
||||
* parsing errors, encoding errors, if the CRL is expired, or if the CRL is older than the
|
||||
* one currently in memory.
|
||||
*/
|
||||
public static void verifyCertificate(
|
||||
X509Certificate rootCert, X509CRL crl, @Tainted X509Certificate cert, Date now)
|
||||
throws GeneralSecurityException {
|
||||
cert.checkValidity(checkNotNull(now, "now"));
|
||||
cert.verify(rootCert.getPublicKey());
|
||||
if (crl.isRevoked(cert)) {
|
||||
X509CRLEntry entry = crl.getRevokedCertificate(cert);
|
||||
throw new CertificateRevokedException(
|
||||
checkNotNull(entry.getRevocationDate(), "revocationDate"),
|
||||
Optional.ofNullable(entry.getRevocationReason()).orElse(CRLReason.UNSPECIFIED),
|
||||
firstNonNull(entry.getCertificateIssuer(), crl.getIssuerX500Principal()),
|
||||
ImmutableMap.of());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an X.509 CRL you downloaded can safely replace your current CRL.
|
||||
*
|
||||
* <p>This routine makes sure {@code newCrl} is signed by {@code rootCert} and that its timestamps
|
||||
* are correct with respect to {@code now}.
|
||||
*
|
||||
* @throws GeneralSecurityException for unsupported protocols, certs not signed by the TMCH,
|
||||
* incorrect keys, and for invalid, old, not-yet-valid or revoked certificates.
|
||||
*/
|
||||
public static void verifyCrl(
|
||||
X509Certificate rootCert, X509CRL oldCrl, @Tainted X509CRL newCrl, Date now)
|
||||
throws GeneralSecurityException {
|
||||
if (newCrl.getThisUpdate().before(oldCrl.getThisUpdate())) {
|
||||
throw new CRLException(String.format(
|
||||
"New CRL is more out of date than our current CRL. %s < %s\n%s",
|
||||
newCrl.getThisUpdate(), oldCrl.getThisUpdate(), newCrl));
|
||||
}
|
||||
if (newCrl.getNextUpdate().before(now)) {
|
||||
throw new CRLException("CRL has expired.\n" + newCrl);
|
||||
}
|
||||
newCrl.verify(rootCert.getPublicKey());
|
||||
}
|
||||
|
||||
private X509Utils() {}
|
||||
}
|
33
util/src/main/java/google/registry/util/XmlEnumUtils.java
Normal file
33
util/src/main/java/google/registry/util/XmlEnumUtils.java
Normal file
|
@ -0,0 +1,33 @@
|
|||
// 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.util;
|
||||
|
||||
import javax.xml.bind.annotation.XmlEnumValue;
|
||||
|
||||
/** Utility methods related to xml enums. */
|
||||
public class XmlEnumUtils {
|
||||
/** Read the {@link XmlEnumValue} string off of an enum. */
|
||||
public static String enumToXml(Enum<?> input) {
|
||||
try {
|
||||
return input
|
||||
.getDeclaringClass()
|
||||
.getField(input.name())
|
||||
.getAnnotation(XmlEnumValue.class)
|
||||
.value();
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
56
util/src/main/java/google/registry/util/XmlToEnumMapper.java
Normal file
56
util/src/main/java/google/registry/util/XmlToEnumMapper.java
Normal file
|
@ -0,0 +1,56 @@
|
|||
// 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.util;
|
||||
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import javax.xml.bind.annotation.XmlEnumValue;
|
||||
|
||||
/** Efficient lookup from xml enums to java enums */
|
||||
public final class XmlToEnumMapper<T extends Enum<?>> {
|
||||
|
||||
private final ImmutableMap<String, T> map;
|
||||
|
||||
/** Look up T from the {@link XmlEnumValue} */
|
||||
public T xmlToEnum(String value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link XmlToEnumMapper} from xml value to enum value.
|
||||
*/
|
||||
public static <T extends Enum<?>> XmlToEnumMapper<T> create(T[] enumValues) {
|
||||
return new XmlToEnumMapper<>(enumValues);
|
||||
}
|
||||
|
||||
private XmlToEnumMapper(T[] enumValues) {
|
||||
ImmutableMap.Builder<String, T> mapBuilder = new ImmutableMap.Builder<>();
|
||||
for (T value : enumValues) {
|
||||
try {
|
||||
XmlEnumValue xmlAnnotation = value
|
||||
.getDeclaringClass()
|
||||
.getField(value.name())
|
||||
.getAnnotation(XmlEnumValue.class);
|
||||
checkArgumentNotNull(xmlAnnotation, "Cannot map enum value to xml name: " + value);
|
||||
String xmlName = xmlAnnotation.value();
|
||||
mapBuilder = mapBuilder.put(xmlName, value);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
map = mapBuilder.build();
|
||||
}
|
||||
}
|
119
util/src/main/java/google/registry/util/YamlUtils.java
Normal file
119
util/src/main/java/google/registry/util/YamlUtils.java
Normal file
|
@ -0,0 +1,119 @@
|
|||
// 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.util;
|
||||
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* Utility methods for dealing with YAML.
|
||||
*
|
||||
* <p>There are always two YAML configuration files that are used: the {@code default-config.yaml}
|
||||
* file, which contains default configuration for all environments, and the environment-specific
|
||||
* {@code nomulus-config-ENVIRONMENT.yaml} file, which contains overrides for the default values for
|
||||
* environment-specific settings such as the App Engine project ID. The environment-specific
|
||||
* configuration can be blank, but it must exist.
|
||||
*/
|
||||
public final class YamlUtils {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/**
|
||||
* Loads the POJO of type {@code T} from merged YAML configuration files.
|
||||
*
|
||||
* @param defaultYaml content of the default YAML file.
|
||||
* @param customYaml content of the custom YAML file, to override default values.
|
||||
* @param clazz type of the POJO loaded from the merged YAML files.
|
||||
* @throws IllegalStateException if the configuration files don't exist or are invalid
|
||||
*/
|
||||
public static <T> T getConfigSettings(String defaultYaml, String customYaml, Class<T> clazz) {
|
||||
try {
|
||||
String mergedYaml = mergeYaml(defaultYaml, customYaml);
|
||||
return new Yaml().loadAs(mergedYaml, clazz);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(
|
||||
"Fatal error: Environment configuration YAML file is invalid", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges two YAML documents together.
|
||||
*
|
||||
* <p>Any fields that are specified in customYaml will override fields of the same path in
|
||||
* defaultYaml. Additional fields in customYaml that aren't specified in defaultYaml will be
|
||||
* ignored. The schemas of all fields that are present must be identical, e.g. it is an error to
|
||||
* override a field that has a Map value in the default YAML with a field of any other type in the
|
||||
* custom YAML.
|
||||
*
|
||||
* <p>Only maps are handled recursively; lists are simply overridden in place as-is, as are maps
|
||||
* whose name is suffixed with "Map" -- this allows entire maps to be overridden, rather than
|
||||
* merged.
|
||||
*/
|
||||
static String mergeYaml(String defaultYaml, String customYaml) {
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> yamlMap = loadAsMap(yaml, defaultYaml).get();
|
||||
Optional<Map<String, Object>> customMap = loadAsMap(yaml, customYaml);
|
||||
if (customMap.isPresent()) {
|
||||
yamlMap = mergeMaps(yamlMap, customMap.get());
|
||||
logger.atFine().log("Successfully loaded environment configuration YAML file.");
|
||||
} else {
|
||||
logger.atWarning().log("Ignoring empty environment configuration YAML file.");
|
||||
}
|
||||
return yaml.dump(yamlMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges a custom map into a default map, and returns the merged result.
|
||||
*
|
||||
* <p>All keys in the default map that are also specified in the custom map are overridden with
|
||||
* the custom map's value. This runs recursively on all contained maps.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> mergeMaps(
|
||||
Map<String, Object> defaultMap, Map<String, Object> customMap) {
|
||||
for (String key : defaultMap.keySet()) {
|
||||
if (!customMap.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
Object newValue;
|
||||
if (defaultMap.get(key) instanceof Map && !key.endsWith("Map")) {
|
||||
newValue =
|
||||
mergeMaps(
|
||||
(Map<String, Object>) defaultMap.get(key),
|
||||
(Map<String, Object>) customMap.get(key));
|
||||
} else {
|
||||
newValue = customMap.get(key);
|
||||
}
|
||||
defaultMap.put(key, newValue);
|
||||
}
|
||||
return defaultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a structured map loaded from a YAML config string.
|
||||
*
|
||||
* <p>If the YAML string is empty or does not contain any data (e.g. it's only comments), then
|
||||
* absent is returned.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Optional<Map<String, Object>> loadAsMap(Yaml yaml, String yamlString) {
|
||||
return Optional.ofNullable((Map<String, Object>) yaml.load(yamlString));
|
||||
}
|
||||
|
||||
private YamlUtils() {}
|
||||
}
|
16
util/src/main/java/google/registry/util/package-info.java
Normal file
16
util/src/main/java/google/registry/util/package-info.java
Normal file
|
@ -0,0 +1,16 @@
|
|||
// 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.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.util;
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright 2018 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.appengine.api.modules.ModulesService;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link AppEngineServiceUtilsImpl}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class AppEngineServiceUtilsImplTest {
|
||||
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock private ModulesService modulesService;
|
||||
|
||||
private AppEngineServiceUtils appEngineServiceUtils;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
appEngineServiceUtils = new AppEngineServiceUtilsImpl(modulesService);
|
||||
when(modulesService.getVersionHostname(anyString(), isNull()))
|
||||
.thenReturn("1234.servicename.projectid.appspot.fake");
|
||||
when(modulesService.getVersionHostname(anyString(), eq("2345")))
|
||||
.thenReturn("2345.servicename.projectid.appspot.fake");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getServiceHostname_doesntIncludeVersionId() {
|
||||
assertThat(appEngineServiceUtils.getServiceHostname("servicename"))
|
||||
.isEqualTo("servicename.projectid.appspot.fake");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getVersionHostname_doesIncludeVersionId() {
|
||||
assertThat(appEngineServiceUtils.getCurrentVersionHostname("servicename"))
|
||||
.isEqualTo("1234.servicename.projectid.appspot.fake");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getVersionHostname_worksWithVersionId() {
|
||||
assertThat(appEngineServiceUtils.getVersionHostname("servicename", "2345"))
|
||||
.isEqualTo("2345.servicename.projectid.appspot.fake");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getVersionHostname_throwsWhenVersionIdIsNull() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> appEngineServiceUtils.getVersionHostname("servicename", null));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Must specify the version");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setNumInstances_worksWithValidParameters() {
|
||||
appEngineServiceUtils.setNumInstances("service", "version", 10L);
|
||||
verify(modulesService, times(1)).setNumInstances("service", "version", 10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setNumInstances_throwsWhenServiceIsNull() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> appEngineServiceUtils.setNumInstances(null, "version", 10L));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Must specify the service");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setNumInstances_throwsWhenVersionIsNull() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> appEngineServiceUtils.setNumInstances("service", null, 10L));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Must specify the version");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_setNumInstances_throwsWhenNumInstancesIsInvalid() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> appEngineServiceUtils.setNumInstances("service", "version", -10L));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Number of instances must be greater than 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_convertToSingleSubdomain_doesNothingWithoutServiceOrHostname() {
|
||||
assertThat(appEngineServiceUtils.convertToSingleSubdomain("projectid.appspot.com"))
|
||||
.isEqualTo("projectid.appspot.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_convertToSingleSubdomain_doesNothingWhenItCannotParseCorrectly() {
|
||||
assertThat(appEngineServiceUtils.convertToSingleSubdomain("garbage.notrealhost.example"))
|
||||
.isEqualTo("garbage.notrealhost.example");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_convertToSingleSubdomain_convertsWithServiceName() {
|
||||
assertThat(appEngineServiceUtils.convertToSingleSubdomain("service.projectid.appspot.com"))
|
||||
.isEqualTo("service-dot-projectid.appspot.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_convertToSingleSubdomain_convertsWithVersionAndServiceName() {
|
||||
assertThat(
|
||||
appEngineServiceUtils.convertToSingleSubdomain("version.service.projectid.appspot.com"))
|
||||
.isEqualTo("version-dot-service-dot-projectid.appspot.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_convertToSingleSubdomain_convertsWithInstanceAndVersionAndServiceName() {
|
||||
assertThat(
|
||||
appEngineServiceUtils.convertToSingleSubdomain(
|
||||
"instanceid.version.service.projectid.appspot.com"))
|
||||
.isEqualTo("instanceid-dot-version-dot-service-dot-projectid.appspot.com");
|
||||
}
|
||||
}
|
34
util/src/test/java/google/registry/util/BUILD
Normal file
34
util/src/test/java/google/registry/util/BUILD
Normal file
|
@ -0,0 +1,34 @@
|
|||
package(
|
||||
default_testonly = 1,
|
||||
default_visibility = ["//java/google/registry:registry_project"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules")
|
||||
|
||||
java_library(
|
||||
name = "util",
|
||||
srcs = glob(["*.java"]),
|
||||
deps = [
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_flogger",
|
||||
"@com_google_flogger_system_backend",
|
||||
"@com_google_guava",
|
||||
"@com_google_guava_testlib",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_mockito_core",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(["*Test.java"]),
|
||||
deps = [":util"],
|
||||
)
|
|
@ -0,0 +1,315 @@
|
|||
// 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.util;
|
||||
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import com.google.common.testing.NullPointerTester;
|
||||
import com.google.common.testing.SerializableTester;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Tests for {@link CidrAddressBlock}.
|
||||
*
|
||||
*/
|
||||
public class CidrAddressBlockTest extends TestCase {
|
||||
|
||||
public void testNulls() {
|
||||
NullPointerTester tester = new NullPointerTester();
|
||||
tester.testAllPublicStaticMethods(CidrAddressBlock.class);
|
||||
tester.testAllPublicConstructors(CidrAddressBlock.class);
|
||||
tester.testAllPublicInstanceMethods(new CidrAddressBlock("::/0"));
|
||||
}
|
||||
|
||||
public void testConstructorWithNetmask() {
|
||||
CidrAddressBlock b0 = new CidrAddressBlock("22.24.66.0/24");
|
||||
assertEquals("22.24.66.0", b0.getIp());
|
||||
assertEquals(24, b0.getNetmask());
|
||||
}
|
||||
|
||||
public void testConstructorPicksNetmask() {
|
||||
CidrAddressBlock b0 = new CidrAddressBlock("64.132.1.2");
|
||||
assertEquals(32, b0.getNetmask());
|
||||
}
|
||||
|
||||
public void testConstructorDoesntThrow() {
|
||||
new CidrAddressBlock("64.132.0.0/16");
|
||||
new CidrAddressBlock("128.142.217.0/24");
|
||||
new CidrAddressBlock("35.213.0.0", 16);
|
||||
new CidrAddressBlock("89.23.164.0", 24);
|
||||
}
|
||||
|
||||
public void testInetAddressConstructor() {
|
||||
CidrAddressBlock b0 = new CidrAddressBlock(InetAddresses.forString("1.2.3.4"));
|
||||
assertEquals(32, b0.getNetmask());
|
||||
assertEquals("1.2.3.4", b0.getIp());
|
||||
|
||||
CidrAddressBlock b1 = new CidrAddressBlock("2001:db8::/32");
|
||||
assertEquals(InetAddresses.forString("2001:db8::"), b1.getInetAddress());
|
||||
assertEquals(32, b1.getNetmask());
|
||||
b1 = new CidrAddressBlock("2001:db8::1");
|
||||
assertEquals(128, b1.getNetmask());
|
||||
b1 = new CidrAddressBlock(InetAddresses.forString("5ffe::1"));
|
||||
assertEquals(128, b1.getNetmask());
|
||||
assertEquals("5ffe:0:0:0:0:0:0:1", b1.getIp());
|
||||
}
|
||||
|
||||
public void testCornerCasesSucceed() {
|
||||
new CidrAddressBlock("0.0.0.0/32");
|
||||
new CidrAddressBlock("255.255.255.255/32");
|
||||
new CidrAddressBlock("255.255.255.254/31");
|
||||
new CidrAddressBlock("128.0.0.0/1");
|
||||
new CidrAddressBlock("0.0.0.0/0");
|
||||
new CidrAddressBlock("::");
|
||||
new CidrAddressBlock("::/128");
|
||||
new CidrAddressBlock("::/0");
|
||||
new CidrAddressBlock("8000::/1");
|
||||
new CidrAddressBlock("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
|
||||
new CidrAddressBlock("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
|
||||
new CidrAddressBlock("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/127");
|
||||
}
|
||||
|
||||
public void testFailure() {
|
||||
assertConstructionFails("");
|
||||
assertConstructionFails("0");
|
||||
assertConstructionFails("1");
|
||||
assertConstructionFails("alskjewosvdhfshjwklerjlkj");
|
||||
assertConstructionFails("lawkejrlaksdj/24");
|
||||
assertConstructionFails("192.168.34.23/awlejkrhlsdhf");
|
||||
assertConstructionFails("192.168.34.23/");
|
||||
assertConstructionFails("192.168.34.23/-1");
|
||||
assertConstructionFails("192.239.0.0/12");
|
||||
assertConstructionFails("192.168.223.15/33");
|
||||
assertConstructionFails("268.23.53.0/24");
|
||||
assertConstructionFails("192..23.53.0/24");
|
||||
assertConstructionFails("192..53.0/24");
|
||||
|
||||
assertConstructionFails("192.23.230.0/16");
|
||||
assertConstructionFails("192.23.255.0/16");
|
||||
assertConstructionFails("192.23.18.1/16");
|
||||
assertConstructionFails("123.34.111.240/35");
|
||||
|
||||
assertConstructionFails("160.32.34.23", 240);
|
||||
assertConstructionFails("alskjewosvdhfshjwklerjlkj", 24);
|
||||
assertConstructionFails("160.32.34.23", 1);
|
||||
|
||||
assertConstructionFails("2001:db8::1/");
|
||||
assertConstructionFails("2001:db8::1", -1);
|
||||
assertConstructionFails("2001:db8::1", 0);
|
||||
assertConstructionFails("2001:db8::1", 32);
|
||||
assertConstructionFails("2001:db8::1", 129);
|
||||
assertConstructionFails("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/127");
|
||||
}
|
||||
|
||||
public void testTruncation() {
|
||||
ImmutableMap<String, String> netblocks = new ImmutableMap.Builder<String, String>()
|
||||
// IPv4
|
||||
.put("1.2.3.4/0", "0.0.0.0/0")
|
||||
.put("1.2.3.4/24", "1.2.3.0/24")
|
||||
.put("1.2.3.255/27", "1.2.3.224/27")
|
||||
.put("1.2.3.255/28", "1.2.3.240/28")
|
||||
// IPv6
|
||||
.put("2001:db8::1/0", "::/0")
|
||||
.put("2001:db8::1/16", "2001::/16")
|
||||
.put("2001:db8::1/21", "2001:800::/21")
|
||||
.put("2001:db8::1/22", "2001:c00::/22")
|
||||
.build();
|
||||
for (Map.Entry<String, String> pair : netblocks.entrySet()) {
|
||||
assertConstructionFails(pair.getKey());
|
||||
assertEquals(
|
||||
new CidrAddressBlock(pair.getValue()),
|
||||
CidrAddressBlock.create(pair.getKey()));
|
||||
assertEquals(
|
||||
CidrAddressBlock.create(pair.getKey()),
|
||||
CidrAddressBlock.create(pair.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
public void testContains() {
|
||||
CidrAddressBlock b0 = CidrAddressBlock.create("172.24.255.0/24");
|
||||
assertTrue(b0.contains(b0));
|
||||
assertTrue(b0.contains(b0.getIp()));
|
||||
assertTrue(b0.contains(b0.getInetAddress()));
|
||||
|
||||
/*
|
||||
* Test an "IPv4 compatible" IPv6 address.
|
||||
*
|
||||
* "IPv4 compatible" addresses are not IPv4 addresses. They are
|
||||
* written this way for "convenience" and appear on the wire as
|
||||
* 128bit address with 96 leading bits of 0.
|
||||
*/
|
||||
assertFalse(b0.contains("::172.24.255.0"));
|
||||
|
||||
/*
|
||||
* Test an "IPv4 mapped" IPv6 address.
|
||||
*
|
||||
* "IPv4 mapped" addresses in Java create only Inet4Address objects.
|
||||
* For more detailed history see the discussion of "mapped" addresses
|
||||
* in com.google.common.net.InetAddresses.
|
||||
*/
|
||||
assertTrue(b0.contains("::ffff:172.24.255.0"));
|
||||
|
||||
assertFalse(b0.contains((InetAddress) null));
|
||||
assertFalse(b0.contains((CidrAddressBlock) null));
|
||||
assertFalse(b0.contains((String) null));
|
||||
assertFalse(b0.contains("bogus IP address or CIDR block"));
|
||||
|
||||
CidrAddressBlock b1 = CidrAddressBlock.create("172.24.255.0/23");
|
||||
assertFalse(b0.contains(b1));
|
||||
assertTrue(b1.contains(b0));
|
||||
|
||||
CidrAddressBlock b2 = CidrAddressBlock.create("2001:db8::/48");
|
||||
assertFalse(b0.contains(b2));
|
||||
assertFalse(b0.contains(b2.getIp()));
|
||||
assertFalse(b0.contains(b2.getInetAddress()));
|
||||
assertFalse(b1.contains(b2));
|
||||
assertFalse(b1.contains(b2.getIp()));
|
||||
assertFalse(b1.contains(b2.getInetAddress()));
|
||||
assertFalse(b2.contains(b0));
|
||||
assertFalse(b2.contains(b0.getIp()));
|
||||
assertFalse(b2.contains(b0.getInetAddress()));
|
||||
assertFalse(b2.contains(b1));
|
||||
assertFalse(b2.contains(b1.getIp()));
|
||||
assertFalse(b2.contains(b1.getInetAddress()));
|
||||
assertTrue(b2.contains(b2));
|
||||
assertTrue(b2.contains(b2.getIp()));
|
||||
assertTrue(b2.contains(b2.getInetAddress()));
|
||||
|
||||
CidrAddressBlock b3 = CidrAddressBlock.create("2001:db8::/32");
|
||||
assertFalse(b2.contains(b3));
|
||||
assertTrue(b3.contains(b2));
|
||||
|
||||
CidrAddressBlock allIPv4 = CidrAddressBlock.create("0.0.0.0/0");
|
||||
assertTrue(allIPv4.contains(b0));
|
||||
assertTrue(allIPv4.contains(b1));
|
||||
assertFalse(b0.contains(allIPv4));
|
||||
assertFalse(b1.contains(allIPv4));
|
||||
assertFalse(allIPv4.contains(b2));
|
||||
assertFalse(allIPv4.contains(b3));
|
||||
assertFalse(b2.contains(allIPv4));
|
||||
assertFalse(b3.contains(allIPv4));
|
||||
assertFalse(allIPv4.contains("::172.24.255.0"));
|
||||
assertTrue(allIPv4.contains("::ffff:172.24.255.0"));
|
||||
|
||||
CidrAddressBlock allIPv6 = CidrAddressBlock.create("::/0");
|
||||
assertTrue(allIPv6.contains(b2));
|
||||
assertTrue(allIPv6.contains(b3));
|
||||
assertFalse(b2.contains(allIPv6));
|
||||
assertFalse(b3.contains(allIPv6));
|
||||
assertFalse(allIPv6.contains(b0));
|
||||
assertFalse(allIPv6.contains(b1));
|
||||
assertFalse(b0.contains(allIPv6));
|
||||
assertFalse(b1.contains(allIPv6));
|
||||
assertTrue(allIPv6.contains("::172.24.255.0"));
|
||||
assertFalse(allIPv6.contains("::ffff:172.24.255.0"));
|
||||
|
||||
assertFalse(allIPv4.contains(allIPv6));
|
||||
assertFalse(allIPv6.contains(allIPv4));
|
||||
}
|
||||
|
||||
public void testGetAllOnesAddress() {
|
||||
// <CIDR block> -> <expected getAllOnesAddress()>
|
||||
ImmutableMap<String, String> testCases = new ImmutableMap.Builder<String, String>()
|
||||
.put("172.24.255.0/24", "172.24.255.255")
|
||||
.put("172.24.0.0/15", "172.25.255.255")
|
||||
.put("172.24.254.0/23", "172.24.255.255")
|
||||
.put("172.24.255.0/32", "172.24.255.0")
|
||||
.put("0.0.0.0/0", "255.255.255.255")
|
||||
.put("2001:db8::/48", "2001:db8::ffff:ffff:ffff:ffff:ffff")
|
||||
.put("2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff")
|
||||
.put("2001:db8::/128", "2001:db8::")
|
||||
.put("::/0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
|
||||
.build();
|
||||
|
||||
for (Map.Entry<String, String> testCase : testCases.entrySet()) {
|
||||
assertEquals(
|
||||
InetAddresses.forString(testCase.getValue()),
|
||||
CidrAddressBlock.create(testCase.getKey()).getAllOnesAddress());
|
||||
}
|
||||
}
|
||||
|
||||
public void testEqualsAndHashCode() {
|
||||
CidrAddressBlock b0 = new CidrAddressBlock("172.24.66.0/24");
|
||||
CidrAddressBlock b1 = new CidrAddressBlock("172.24.66.0", 24);
|
||||
CidrAddressBlock b2 = new CidrAddressBlock("172.24.0.0/16");
|
||||
CidrAddressBlock b3 = new CidrAddressBlock("172.24.65.0/24");
|
||||
assertEquals(b0, b1);
|
||||
assertEquals(b0, new CidrAddressBlock(b0.toString()));
|
||||
assertEquals(b0.hashCode(), b1.hashCode());
|
||||
assertTrue(!b0.equals(b2));
|
||||
assertTrue(!b0.equals(b3));
|
||||
|
||||
b0 = new CidrAddressBlock("2001:db8::/64");
|
||||
b1 = new CidrAddressBlock("2001:0DB8:0:0::", 64);
|
||||
b2 = new CidrAddressBlock("2001:db8::/32");
|
||||
b3 = new CidrAddressBlock("2001:0DB8:0:1::", 64);
|
||||
assertEquals(b0, b1);
|
||||
assertEquals(b0, new CidrAddressBlock(b0.toString()));
|
||||
assertEquals(b0.hashCode(), b1.hashCode());
|
||||
assertNotEquals(b0, b2);
|
||||
assertNotEquals(b0, b3);
|
||||
}
|
||||
|
||||
public void testIterate() {
|
||||
CidrAddressBlock b0 = new CidrAddressBlock("172.24.66.0/24");
|
||||
int count = 0;
|
||||
for (InetAddress addr : b0) {
|
||||
assertTrue(b0.contains(addr));
|
||||
++count;
|
||||
}
|
||||
assertEquals(256, count);
|
||||
|
||||
CidrAddressBlock b1 = new CidrAddressBlock("2001:0DB8:0:0::/120");
|
||||
count = 0;
|
||||
for (InetAddress addr : b1) {
|
||||
assertTrue(b1.contains(addr));
|
||||
++count;
|
||||
}
|
||||
assertEquals(256, count);
|
||||
|
||||
CidrAddressBlock b2 = new CidrAddressBlock("255.255.255.254/31");
|
||||
Iterator<InetAddress> i = b2.iterator();
|
||||
i.next();
|
||||
i.next();
|
||||
assertThrows(NoSuchElementException.class, i::next);
|
||||
}
|
||||
|
||||
public void testSerializability() {
|
||||
SerializableTester.reserializeAndAssert(new CidrAddressBlock("22.24.66.0/24"));
|
||||
SerializableTester.reserializeAndAssert(new CidrAddressBlock("64.132.1.2"));
|
||||
SerializableTester.reserializeAndAssert(
|
||||
new CidrAddressBlock(InetAddresses.forString("1.2.3.4")));
|
||||
|
||||
SerializableTester.reserializeAndAssert(new CidrAddressBlock("2001:db8::/32"));
|
||||
SerializableTester.reserializeAndAssert(new CidrAddressBlock("2001:db8::1"));
|
||||
SerializableTester.reserializeAndAssert(
|
||||
new CidrAddressBlock(InetAddresses.forString("5ffe::1")));
|
||||
}
|
||||
|
||||
private static void assertConstructionFails(String ip) {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CidrAddressBlock(ip));
|
||||
}
|
||||
|
||||
private static void assertConstructionFails(String ip, int netmask) {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CidrAddressBlock(ip, netmask));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.CollectionUtils.partitionMap;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.Map;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link CollectionUtils} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CollectionUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testNullToEmptyMap_leavesNonNullAlone() {
|
||||
Map<String, Integer> map = ImmutableMap.of("hello", 1);
|
||||
assertThat(nullToEmpty(map)).isEqualTo(map);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullToEmptyMap_convertsNullToEmptyMap() {
|
||||
Map<String, Integer> map = null;
|
||||
Map<String, Integer> convertedMap = nullToEmpty(map);
|
||||
assertThat(map).isNull();
|
||||
assertThat(convertedMap).isNotNull();
|
||||
assertThat(convertedMap).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionMap() {
|
||||
Map<String, String> map = ImmutableMap.of("ka", "va", "kb", "vb", "kc", "vc");
|
||||
assertThat(partitionMap(map, 2)).containsExactlyElementsIn(ImmutableList.of(
|
||||
ImmutableMap.of("ka", "va", "kb", "vb"),
|
||||
ImmutableMap.of("kc", "vc")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionMap_emptyInput() {
|
||||
assertThat(partitionMap(ImmutableMap.of(), 100)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionMap_negativePartitionSize() {
|
||||
assertThrows(IllegalArgumentException.class, () -> partitionMap(ImmutableMap.of("A", "b"), -2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionMap_nullMap() {
|
||||
assertThrows(NullPointerException.class, () -> partitionMap(null, 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeadCodeWeDontWantToDelete() {
|
||||
CollectionUtils.nullToEmpty(HashMultimap.create());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ComparingInvocationHandler}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ComparingInvocationHandlerTest {
|
||||
|
||||
static class Dummy {}
|
||||
|
||||
interface MyInterface {
|
||||
String func(int a, String b);
|
||||
|
||||
Dummy func();
|
||||
}
|
||||
|
||||
static class MyException extends RuntimeException {
|
||||
MyException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static class MyOtherException extends RuntimeException {
|
||||
MyOtherException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
static final ArrayList<String> log = new ArrayList<>();
|
||||
|
||||
static final class MyInterfaceComparingInvocationHandler
|
||||
extends ComparingInvocationHandler<MyInterface> {
|
||||
|
||||
private boolean dummyEqualsResult = true;
|
||||
private boolean exceptionEqualsResult = true;
|
||||
|
||||
MyInterfaceComparingInvocationHandler(MyInterface actual, MyInterface second) {
|
||||
super(MyInterface.class, actual, second);
|
||||
}
|
||||
|
||||
MyInterfaceComparingInvocationHandler setExeptionsEquals(boolean result) {
|
||||
this.exceptionEqualsResult = result;
|
||||
return this;
|
||||
}
|
||||
|
||||
MyInterfaceComparingInvocationHandler setDummyEquals(boolean result) {
|
||||
this.dummyEqualsResult = result;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void log(Method method, String message) {
|
||||
log.add(String.format("%s: %s", method.getName(), message));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean compareResults(Method method, @Nullable Object a, @Nullable Object b) {
|
||||
if (method.getReturnType().equals(Dummy.class)) {
|
||||
return dummyEqualsResult;
|
||||
}
|
||||
return super.compareResults(method, a, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String stringifyResult(Method method, @Nullable Object a) {
|
||||
if (method.getReturnType().equals(Dummy.class)) {
|
||||
return "dummy";
|
||||
}
|
||||
return super.stringifyResult(method, a);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean compareThrown(Method method, Throwable a, Throwable b) {
|
||||
return exceptionEqualsResult && super.compareThrown(method, a, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String stringifyThrown(Method method, Throwable a) {
|
||||
return String.format("testException(%s)", super.stringifyThrown(method, a));
|
||||
}
|
||||
}
|
||||
|
||||
private static final String ACTUAL_RESULT = "actual result";
|
||||
private static final String SECOND_RESULT = "second result";
|
||||
|
||||
private final MyInterface myActualMock = mock(MyInterface.class);
|
||||
private final MyInterface mySecondMock = mock(MyInterface.class);
|
||||
private MyInterfaceComparingInvocationHandler invocationHandler;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
log.clear();
|
||||
invocationHandler = new MyInterfaceComparingInvocationHandler(myActualMock, mySecondMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_actualThrows_logDifference() {
|
||||
MyInterface comparator = invocationHandler.makeProxy();
|
||||
MyException myException = new MyException("message");
|
||||
when(myActualMock.func(3, "str")).thenThrow(myException);
|
||||
when(mySecondMock.func(3, "str")).thenReturn(SECOND_RESULT);
|
||||
|
||||
assertThrows(MyException.class, () -> comparator.func(3, "str"));
|
||||
assertThat(log)
|
||||
.containsExactly(
|
||||
String.format(
|
||||
"func: Only actual implementation threw exception: testException(%s)",
|
||||
myException.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_secondThrows_logDifference() {
|
||||
MyInterface comparator = invocationHandler.makeProxy();
|
||||
MyOtherException myOtherException = new MyOtherException("message");
|
||||
when(myActualMock.func(3, "str")).thenReturn(ACTUAL_RESULT);
|
||||
when(mySecondMock.func(3, "str")).thenThrow(myOtherException);
|
||||
|
||||
assertThat(comparator.func(3, "str")).isEqualTo(ACTUAL_RESULT);
|
||||
|
||||
assertThat(log)
|
||||
.containsExactly(
|
||||
String.format(
|
||||
"func: Only second implementation threw exception: testException(%s)",
|
||||
myOtherException.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_bothThrowEqual_noLog() {
|
||||
MyInterface comparator = invocationHandler.setExeptionsEquals(true).makeProxy();
|
||||
MyException myException = new MyException("actual message");
|
||||
MyOtherException myOtherException = new MyOtherException("second message");
|
||||
when(myActualMock.func(3, "str")).thenThrow(myException);
|
||||
when(mySecondMock.func(3, "str")).thenThrow(myOtherException);
|
||||
|
||||
assertThrows(MyException.class, () -> comparator.func(3, "str"));
|
||||
assertThat(log).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_bothThrowDifferent_logDifference() {
|
||||
MyInterface comparator = invocationHandler.setExeptionsEquals(false).makeProxy();
|
||||
MyException myException = new MyException("actual message");
|
||||
MyOtherException myOtherException = new MyOtherException("second message");
|
||||
when(myActualMock.func(3, "str")).thenThrow(myException);
|
||||
when(mySecondMock.func(3, "str")).thenThrow(myOtherException);
|
||||
|
||||
assertThrows(MyException.class, () -> comparator.func(3, "str"));
|
||||
assertThat(log)
|
||||
.containsExactly(
|
||||
String.format(
|
||||
"func: Both implementations threw, but got different exceptions! "
|
||||
+ "'testException(%s)' vs 'testException(%s)'",
|
||||
myException.toString(), myOtherException.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_bothReturnSame_noLog() {
|
||||
MyInterface comparator = invocationHandler.makeProxy();
|
||||
when(myActualMock.func(3, "str")).thenReturn(ACTUAL_RESULT);
|
||||
when(mySecondMock.func(3, "str")).thenReturn(ACTUAL_RESULT);
|
||||
|
||||
assertThat(comparator.func(3, "str")).isEqualTo(ACTUAL_RESULT);
|
||||
|
||||
assertThat(log).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_bothReturnDifferent_logDifference() {
|
||||
MyInterface comparator = invocationHandler.makeProxy();
|
||||
when(myActualMock.func(3, "str")).thenReturn(ACTUAL_RESULT);
|
||||
when(mySecondMock.func(3, "str")).thenReturn(SECOND_RESULT);
|
||||
|
||||
assertThat(comparator.func(3, "str")).isEqualTo(ACTUAL_RESULT);
|
||||
|
||||
assertThat(log)
|
||||
.containsExactly("func: Got different results! 'actual result' vs 'second result'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_usesOverriddenMethods_noDifference() {
|
||||
MyInterface comparator = invocationHandler.setDummyEquals(true).makeProxy();
|
||||
when(myActualMock.func()).thenReturn(new Dummy());
|
||||
when(mySecondMock.func()).thenReturn(new Dummy());
|
||||
|
||||
comparator.func();
|
||||
|
||||
assertThat(log).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_usesOverriddenMethods_logDifference() {
|
||||
MyInterface comparator = invocationHandler.setDummyEquals(false).makeProxy();
|
||||
when(myActualMock.func()).thenReturn(new Dummy());
|
||||
when(mySecondMock.func()).thenReturn(new Dummy());
|
||||
|
||||
comparator.func();
|
||||
|
||||
assertThat(log).containsExactly("func: Got different results! 'dummy' vs 'dummy'");
|
||||
}
|
||||
}
|
71
util/src/test/java/google/registry/util/ConcurrentTest.java
Normal file
71
util/src/test/java/google/registry/util/ConcurrentTest.java
Normal file
|
@ -0,0 +1,71 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.testing.NullPointerTester;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.util.function.Function;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link Concurrent}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ConcurrentTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.withDatastore()
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testTransform_emptyList_returnsEmptyList() {
|
||||
assertThat(Concurrent.transform(ImmutableList.of(), x -> x)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransform_addIntegers() {
|
||||
assertThat(Concurrent.transform(ImmutableList.of(1, 2, 3), input -> input + 1))
|
||||
.containsExactly(2, 3, 4)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransform_throwsException_isSinglyWrappedByUee() {
|
||||
UncheckedExecutionException e =
|
||||
assertThrows(
|
||||
UncheckedExecutionException.class,
|
||||
() ->
|
||||
Concurrent.transform(
|
||||
ImmutableList.of(1, 2, 3),
|
||||
input -> {
|
||||
throw new RuntimeException("hello");
|
||||
}));
|
||||
assertThat(e).hasCauseThat().isInstanceOf(RuntimeException.class);
|
||||
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullness() {
|
||||
NullPointerTester tester = new NullPointerTester().setDefault(Function.class, x -> x);
|
||||
tester.testAllPublicStaticMethods(Concurrent.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.earliestOf;
|
||||
import static google.registry.util.DateTimeUtils.isAtOrAfter;
|
||||
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
|
||||
import static google.registry.util.DateTimeUtils.latestOf;
|
||||
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
|
||||
import static google.registry.util.DateTimeUtils.leapSafeSubtractYears;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link DateTimeUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DateTimeUtilsTest {
|
||||
|
||||
ImmutableList<DateTime> sampleDates = ImmutableList.of(
|
||||
START_OF_TIME, START_OF_TIME.plusDays(1), END_OF_TIME, END_OF_TIME);
|
||||
|
||||
@Test
|
||||
public void testSuccess_earliestOf() {
|
||||
assertThat(earliestOf(START_OF_TIME, END_OF_TIME)).isEqualTo(START_OF_TIME);
|
||||
assertThat(earliestOf(sampleDates)).isEqualTo(START_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_latestOf() {
|
||||
assertThat(latestOf(START_OF_TIME, END_OF_TIME)).isEqualTo(END_OF_TIME);
|
||||
assertThat(latestOf(sampleDates)).isEqualTo(END_OF_TIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_isBeforeOrAt() {
|
||||
assertThat(isBeforeOrAt(START_OF_TIME, START_OF_TIME.plusDays(1))).isTrue();
|
||||
assertThat(isBeforeOrAt(START_OF_TIME, START_OF_TIME)).isTrue();
|
||||
assertThat(isBeforeOrAt(START_OF_TIME.plusDays(1), START_OF_TIME)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_isAtOrAfter() {
|
||||
assertThat(isAtOrAfter(START_OF_TIME, START_OF_TIME.plusDays(1))).isFalse();
|
||||
assertThat(isAtOrAfter(START_OF_TIME, START_OF_TIME)).isTrue();
|
||||
assertThat(isAtOrAfter(START_OF_TIME.plusDays(1), START_OF_TIME)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_leapSafeAddYears() {
|
||||
DateTime startDate = DateTime.parse("2012-02-29T00:00:00Z");
|
||||
assertThat(startDate.plusYears(4)).isEqualTo(DateTime.parse("2016-02-29T00:00:00Z"));
|
||||
assertThat(leapSafeAddYears(startDate, 4)).isEqualTo(DateTime.parse("2016-02-28T00:00:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_leapSafeSubtractYears() {
|
||||
DateTime startDate = DateTime.parse("2012-02-29T00:00:00Z");
|
||||
assertThat(startDate.minusYears(4)).isEqualTo(DateTime.parse("2008-02-29T00:00:00Z"));
|
||||
assertThat(leapSafeSubtractYears(startDate, 4))
|
||||
.isEqualTo(DateTime.parse("2008-02-28T00:00:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_leapSafeSubtractYears_zeroYears() {
|
||||
DateTime leapDay = DateTime.parse("2012-02-29T00:00:00Z");
|
||||
assertThat(leapDay.minusYears(0)).isEqualTo(leapDay);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_earliestOfEmpty() {
|
||||
assertThrows(IllegalArgumentException.class, () -> earliestOf(ImmutableList.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_latestOfEmpty() {
|
||||
assertThrows(IllegalArgumentException.class, () -> earliestOf(ImmutableList.of()));
|
||||
}
|
||||
}
|
107
util/src/test/java/google/registry/util/DiffUtilsTest.java
Normal file
107
util/src/test/java/google/registry/util/DiffUtilsTest.java
Normal file
|
@ -0,0 +1,107 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff;
|
||||
import static google.registry.util.DiffUtils.prettyPrintSetDiff;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link DiffUtils} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DiffUtilsTest {
|
||||
|
||||
@Test
|
||||
public void test_prettyPrintSetDiff_emptySets() {
|
||||
assertThat(prettyPrintSetDiff(ImmutableSet.of(), ImmutableSet.of()))
|
||||
.isEqualTo("NO DIFFERENCES");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prettyPrintSetDiff_noDifferences() {
|
||||
assertThat(prettyPrintSetDiff(ImmutableSet.of("c", "x", "m"), ImmutableSet.of("m", "x", "c")))
|
||||
.isEqualTo("NO DIFFERENCES");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prettyPrintSetDiff_addedElements() {
|
||||
assertThat(prettyPrintSetDiff(ImmutableSet.of("z"), ImmutableSet.of("a", "b", "z")))
|
||||
.isEqualTo("\n ADDED: [a, b]\n FINAL CONTENTS: [a, b, z]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prettyPrintSetDiff_removedElements() {
|
||||
assertThat(prettyPrintSetDiff(ImmutableSet.of("x", "y", "z"), ImmutableSet.of("y")))
|
||||
.isEqualTo("\n REMOVED: [x, z]\n FINAL CONTENTS: [y]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prettyPrintSetDiff_addedAndRemovedElements() {
|
||||
assertThat(prettyPrintSetDiff(
|
||||
ImmutableSet.of("a", "b", "c"), ImmutableSet.of("a", "y", "z")))
|
||||
.isEqualTo("\n ADDED: [y, z]\n REMOVED: [b, c]\n FINAL CONTENTS: [a, y, z]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_emptyToNullCollection_doesntDisplay() {
|
||||
Map<String, Object> mapA = new HashMap<>();
|
||||
mapA.put("a", "jim");
|
||||
mapA.put("b", null);
|
||||
Map<String, Object> mapB = new HashMap<>();
|
||||
mapB.put("a", "tim");
|
||||
mapB.put("b", ImmutableSet.of());
|
||||
// This ensures that it is not outputting a diff of b: null -> [].
|
||||
assertThat(prettyPrintEntityDeepDiff(mapA, mapB)).isEqualTo("a: jim -> tim\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_prettyPrintSetDiff_addedAndRemovedElements_objects() {
|
||||
DummyObject a = DummyObject.create("a");
|
||||
DummyObject b = DummyObject.create("b");
|
||||
DummyObject c = DummyObject.create("c");
|
||||
|
||||
assertThat(prettyPrintSetDiff(
|
||||
ImmutableSet.of(a, b), ImmutableSet.of(a, c)))
|
||||
.isEqualTo("\n"
|
||||
+ " ADDED:\n"
|
||||
+ " {c}\n"
|
||||
+ " REMOVED:\n"
|
||||
+ " {b}\n"
|
||||
+ " FINAL CONTENTS:\n"
|
||||
+ " {a},\n"
|
||||
+ " {c}");
|
||||
}
|
||||
|
||||
private static class DummyObject {
|
||||
public String id;
|
||||
|
||||
public static DummyObject create(String id) {
|
||||
DummyObject instance = new DummyObject();
|
||||
instance.id = id;
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("{%s}", id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static google.registry.util.DomainNameUtils.getSecondLevelDomain;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link DomainNameUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class DomainNameUtilsTest {
|
||||
@Test
|
||||
public void testCanonicalizeDomainName() {
|
||||
assertThat(canonicalizeDomainName("foo")).isEqualTo("foo");
|
||||
assertThat(canonicalizeDomainName("FOO")).isEqualTo("foo");
|
||||
assertThat(canonicalizeDomainName("foo.tld")).isEqualTo("foo.tld");
|
||||
assertThat(canonicalizeDomainName("xn--q9jyb4c")).isEqualTo("xn--q9jyb4c");
|
||||
assertThat(canonicalizeDomainName("XN--Q9JYB4C")).isEqualTo("xn--q9jyb4c");
|
||||
assertThat(canonicalizeDomainName("みんな")).isEqualTo("xn--q9jyb4c");
|
||||
assertThat(canonicalizeDomainName("みんな.みんな")).isEqualTo("xn--q9jyb4c.xn--q9jyb4c");
|
||||
assertThat(canonicalizeDomainName("みんな.foo")).isEqualTo("xn--q9jyb4c.foo");
|
||||
assertThat(canonicalizeDomainName("foo.みんな")).isEqualTo("foo.xn--q9jyb4c");
|
||||
assertThat(canonicalizeDomainName("ħ")).isEqualTo("xn--1ea");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanonicalizeDomainName_acePrefixUnicodeChars() {
|
||||
assertThrows(IllegalArgumentException.class, () -> canonicalizeDomainName("xn--みんな"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSecondLevelDomain_returnsProperDomain() {
|
||||
assertThat(getSecondLevelDomain("foo.bar", "bar")).isEqualTo("foo.bar");
|
||||
assertThat(getSecondLevelDomain("ns1.foo.bar", "bar")).isEqualTo("foo.bar");
|
||||
assertThat(getSecondLevelDomain("ns1.abc.foo.bar", "bar")).isEqualTo("foo.bar");
|
||||
assertThat(getSecondLevelDomain("ns1.abc.foo.bar", "foo.bar")).isEqualTo("abc.foo.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSecondLevelDomain_insufficientDomainNameDepth() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> getSecondLevelDomain("bar", "bar"));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("hostName must be at least one level below the tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSecondLevelDomain_domainNotUnderTld() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> getSecondLevelDomain("foo.bar", "abc"));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("hostName must be under the tld");
|
||||
}
|
||||
}
|
206
util/src/test/java/google/registry/util/HexDumperTest.java
Normal file
206
util/src/test/java/google/registry/util/HexDumperTest.java
Normal file
|
@ -0,0 +1,206 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link HexDumper}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class HexDumperTest {
|
||||
|
||||
@Test
|
||||
public void testEmpty() {
|
||||
String input = "";
|
||||
String output = "[0 bytes total]\n";
|
||||
assertThat(input).isEmpty();
|
||||
assertThat(HexDumper.dumpHex(input.getBytes(UTF_8))).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneLine() {
|
||||
String input = "hello world";
|
||||
String output = "[11 bytes total]\n"
|
||||
+ "00000000 68 65 6c 6c 6f 20 77 6f 72 6c 64 hello world \n";
|
||||
assertThat(input).hasLength(11);
|
||||
assertThat(HexDumper.dumpHex(input.getBytes(UTF_8))).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiLine() {
|
||||
String input = ""
|
||||
+ "\n"
|
||||
+ "Maids heard the goblins cry:\n"
|
||||
+ "\"Come buy our orchard fruits,\n"
|
||||
+ "\"Come buy, come buy:\n";
|
||||
String output = "[81 bytes total]\n"
|
||||
+ "00000000 0a 4d 61 69 64 73 20 68 65 61 72 64 20 74 68 65 .Maids heard the\n"
|
||||
+ "00000016 20 67 6f 62 6c 69 6e 73 20 63 72 79 3a 0a 22 43 goblins cry:.\"C\n"
|
||||
+ "00000032 6f 6d 65 20 62 75 79 20 6f 75 72 20 6f 72 63 68 ome buy our orch\n"
|
||||
+ "00000048 61 72 64 20 66 72 75 69 74 73 2c 0a 22 43 6f 6d ard fruits,.\"Com\n"
|
||||
+ "00000064 65 20 62 75 79 2c 20 63 6f 6d 65 20 62 75 79 3a e buy, come buy:\n"
|
||||
+ "00000080 0a . \n";
|
||||
assertThat(input).hasLength(81);
|
||||
assertThat(HexDumper.dumpHex(input.getBytes(UTF_8))).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFullLine() {
|
||||
String input = "hello worldddddd";
|
||||
String output = "[16 bytes total]\n"
|
||||
+ "00000000 68 65 6c 6c 6f 20 77 6f 72 6c 64 64 64 64 64 64 hello worldddddd\n";
|
||||
assertThat(input).hasLength(16);
|
||||
assertThat(HexDumper.dumpHex(input.getBytes(UTF_8))).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnicode() {
|
||||
String input = "(◕‿◕)";
|
||||
String output = "[11 bytes total]\n"
|
||||
+ "00000000 28 e2 97 95 e2 80 bf e2 97 95 29 (.........) \n";
|
||||
assertThat(input).hasLength(5);
|
||||
assertThat(HexDumper.dumpHex(input.getBytes(UTF_8))).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRainbow() {
|
||||
byte[] input = new byte[256];
|
||||
for (int n = 0; n < 256; ++n) {
|
||||
input[n] = (byte) n;
|
||||
}
|
||||
String output = "[256 bytes total]\n"
|
||||
+ "00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ................\n"
|
||||
+ "00000016 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f ................\n"
|
||||
+ "00000032 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f !\"#$%&'()*+,-./\n"
|
||||
+ "00000048 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 0123456789:;<=>?\n"
|
||||
+ "00000064 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f @ABCDEFGHIJKLMNO\n"
|
||||
+ "00000080 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f PQRSTUVWXYZ[\\]^_\n"
|
||||
+ "00000096 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f `abcdefghijklmno\n"
|
||||
+ "00000112 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f pqrstuvwxyz{|}~.\n"
|
||||
+ "00000128 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f ................\n"
|
||||
+ "00000144 90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f ................\n"
|
||||
+ "00000160 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af ................\n"
|
||||
+ "00000176 b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf ................\n"
|
||||
+ "00000192 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf ................\n"
|
||||
+ "00000208 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df ................\n"
|
||||
+ "00000224 e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef ................\n"
|
||||
+ "00000240 f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff ................\n";
|
||||
assertThat(HexDumper.dumpHex(input)).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLineBuffering() throws Exception {
|
||||
// Assume that we have some data that's N bytes long.
|
||||
byte[] data = "Sweet to tongue and sound to eye; Come buy, come buy.".getBytes(UTF_8);
|
||||
// And a streaming HexDumper that displays N+1 characters per row.
|
||||
int perLine = data.length + 1;
|
||||
try (StringWriter out = new StringWriter();
|
||||
HexDumper dumper = new HexDumper(out, perLine, 0)) {
|
||||
// Constructing the object does not cause it to write anything to our upstream device.
|
||||
assertThat(out.toString()).isEmpty();
|
||||
// And then we write N bytes to the hex dumper.
|
||||
dumper.write(data);
|
||||
// But it won't output any hex because it's buffering a line of output internally.
|
||||
assertThat(out.toString()).isEmpty();
|
||||
// But one more byte will bring the total to N+1, thereby flushing a line of hexdump output.
|
||||
dumper.write(0);
|
||||
assertThat(out.toString())
|
||||
.isEqualTo(
|
||||
"00000000 53 77 65 65 74 20 74 6f 20 74 6f 6e 67 75 65 20 61 6e 64 20 73 6f "
|
||||
+ "75 6e 64 20 74 6f 20 65 79 65 3b 20 43 6f 6d 65 20 62 75 79 2c 20 63 6f 6d 65 "
|
||||
+ "20 62 75 79 2e 00 Sweet to tongue and sound to eye; Come buy, come buy..\n");
|
||||
// No additional data will need to be written upon close.
|
||||
int oldTotal = out.toString().length();
|
||||
assertThat(out.toString().length()).isEqualTo(oldTotal);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlush() throws Exception {
|
||||
try (StringWriter out = new StringWriter();
|
||||
HexDumper dumper = new HexDumper(out)) {
|
||||
dumper.write("hello ".getBytes(UTF_8));
|
||||
assertThat(out.toString()).isEmpty();
|
||||
dumper.flush();
|
||||
assertThat(out.toString()).isEqualTo("00000000 68 65 6c 6c 6f 20 ");
|
||||
dumper.write("world".getBytes(UTF_8));
|
||||
assertThat(out.toString()).isEqualTo("00000000 68 65 6c 6c 6f 20 ");
|
||||
dumper.flush();
|
||||
assertThat(out.toString()).isEqualTo("00000000 68 65 6c 6c 6f 20 77 6f 72 6c 64 ");
|
||||
dumper.close();
|
||||
assertThat(out.toString()).isEqualTo(
|
||||
"00000000 68 65 6c 6c 6f 20 77 6f 72 6c 64 hello world \n");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPerLineIsOne() {
|
||||
String input = "hello";
|
||||
String output = "[5 bytes total]\n"
|
||||
+ "00000000 68 h\n"
|
||||
+ "00000001 65 e\n"
|
||||
+ "00000002 6c l\n"
|
||||
+ "00000003 6c l\n"
|
||||
+ "00000004 6f o\n";
|
||||
assertThat(HexDumper.dumpHex(input.getBytes(UTF_8), 1, 0)).isEqualTo(output);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadArgumentPerLineZero() {
|
||||
HexDumper.dumpHex(new byte[1], 1, 0);
|
||||
assertThrows(IllegalArgumentException.class, () -> HexDumper.dumpHex(new byte[1], 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadArgumentPerLineNegative() {
|
||||
HexDumper.dumpHex(new byte[1], 1, 0);
|
||||
assertThrows(IllegalArgumentException.class, () -> HexDumper.dumpHex(new byte[1], -1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadArgumentPerGroupNegative() {
|
||||
HexDumper.dumpHex(new byte[1], 1, 0);
|
||||
assertThrows(IllegalArgumentException.class, () -> HexDumper.dumpHex(new byte[1], 1, -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadArgumentPerGroupGreaterThanOrEqualToPerLine() {
|
||||
HexDumper.dumpHex(new byte[1], 1, 0);
|
||||
HexDumper.dumpHex(new byte[1], 2, 1);
|
||||
assertThrows(IllegalArgumentException.class, () -> HexDumper.dumpHex(new byte[1], 1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadArgumentBytesIsNull() {
|
||||
HexDumper.dumpHex(new byte[1]);
|
||||
assertThrows(NullPointerException.class, () -> HexDumper.dumpHex(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiClose() throws Exception {
|
||||
try (StringWriter out = new StringWriter();
|
||||
HexDumper dumper = new HexDumper(out)) {
|
||||
dumper.close();
|
||||
dumper.close();
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static google.registry.testing.SystemInfo.hasCommand;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.common.io.Files;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** System integration tests for {@link PosixTarHeader}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class PosixTarHeaderSystemTest {
|
||||
|
||||
@Rule
|
||||
public final TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testCreateSingleFileArchive() throws Exception {
|
||||
assumeTrue(hasCommand("tar"));
|
||||
|
||||
// We have some data (in memory) that we'll call hello.txt.
|
||||
String fileName = "hello.txt";
|
||||
byte[] fileData = "hello world\n".getBytes(UTF_8);
|
||||
|
||||
// We're going to put it in a new tar archive (on the filesystem) named hello.tar.
|
||||
String tarName = "hello.tar";
|
||||
File tarFile = folder.newFile(tarName);
|
||||
try (FileOutputStream output = new FileOutputStream(tarFile)) {
|
||||
output.write(new PosixTarHeader.Builder()
|
||||
.setName(fileName)
|
||||
.setSize(fileData.length)
|
||||
.build()
|
||||
.getBytes());
|
||||
output.write(fileData);
|
||||
output.write(new byte[512 - fileData.length % 512]); // Align with 512-byte block size.
|
||||
output.write(new byte[1024]); // Bunch of null bytes to indicate end of archive.
|
||||
}
|
||||
assertThat(tarFile.length() % 512).isEqualTo(0);
|
||||
assertThat(tarFile.length() / 512).isEqualTo(2 + 2);
|
||||
|
||||
// Now we run the system's tar command to extract our file.
|
||||
String[] cmd = {"tar", "-xf", tarName};
|
||||
String[] env = {"PATH=" + System.getenv("PATH")};
|
||||
File cwd = folder.getRoot();
|
||||
Process pid = Runtime.getRuntime().exec(cmd, env, cwd);
|
||||
String err = CharStreams.toString(new InputStreamReader(pid.getErrorStream(), UTF_8));
|
||||
assertThat(pid.waitFor()).isEqualTo(0);
|
||||
assertThat(err.trim()).isEmpty();
|
||||
|
||||
// And verify that hello.txt came out.
|
||||
File dataFile = new File(cwd, fileName);
|
||||
assertThat(dataFile.exists()).isTrue();
|
||||
assertThat(dataFile.isFile()).isTrue();
|
||||
assertThat(Files.asByteSource(dataFile).read()).isEqualTo(fileData);
|
||||
|
||||
// And that nothing else came out.
|
||||
Set<String> expectedFiles = ImmutableSet.of(tarName, fileName);
|
||||
assertThat(ImmutableSet.copyOf(folder.getRoot().list())).isEqualTo(expectedFiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testCreateMultiFileArchive() throws Exception {
|
||||
assumeTrue(hasCommand("tar"));
|
||||
|
||||
Map<String, String> files = ImmutableMap.of(
|
||||
"one.txt", ""
|
||||
+ "There is data on line one\n"
|
||||
+ "and on line two\n"
|
||||
+ "and on line three\n",
|
||||
"two.txt", ""
|
||||
+ "There is even more data\n"
|
||||
+ "in this second file\n"
|
||||
+ "with its own three lines\n",
|
||||
"subdir/three.txt", ""
|
||||
+ "More data\n"
|
||||
+ "but only two lines\n");
|
||||
|
||||
String tarName = "hello.tar";
|
||||
File tarFile = folder.newFile(tarName);
|
||||
try (FileOutputStream output = new FileOutputStream(tarFile)) {
|
||||
for (String name : files.keySet()) {
|
||||
byte[] data = files.get(name).getBytes(UTF_8);
|
||||
output.write(new PosixTarHeader.Builder()
|
||||
.setName(name)
|
||||
.setSize(data.length)
|
||||
.build()
|
||||
.getBytes());
|
||||
output.write(data);
|
||||
output.write(new byte[512 - data.length % 512]);
|
||||
}
|
||||
output.write(new byte[1024]);
|
||||
}
|
||||
assertThat(tarFile.length() % 512).isEqualTo(0);
|
||||
assertThat(tarFile.length() / 512).isEqualTo(files.size() * 2 + 2);
|
||||
|
||||
String[] cmd = {"tar", "-xf", tarName};
|
||||
String[] env = {"PATH=" + System.getenv("PATH")};
|
||||
File cwd = folder.getRoot();
|
||||
Process pid = Runtime.getRuntime().exec(cmd, env, cwd);
|
||||
String err = CharStreams.toString(new InputStreamReader(pid.getErrorStream(), UTF_8));
|
||||
assertThat(pid.waitFor()).isEqualTo(0);
|
||||
assertThat(err.trim()).isEmpty();
|
||||
|
||||
for (String name : files.keySet()) {
|
||||
File file = new File(folder.getRoot(), name);
|
||||
assertWithMessage(name + " exists").that(file.exists()).isTrue();
|
||||
assertWithMessage(name + " is a file").that(file.isFile()).isTrue();
|
||||
byte[] data = files.get(name).getBytes(UTF_8);
|
||||
assertThat(Files.asByteSource(file).read()).isEqualTo(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testReadArchiveUstar() throws Exception {
|
||||
assumeTrue(hasCommand("tar"));
|
||||
|
||||
String one = "the first line";
|
||||
String two = "the second line";
|
||||
File cwd = folder.getRoot();
|
||||
Files.write(one.getBytes(UTF_8), new File(cwd, "one"));
|
||||
Files.write(two.getBytes(UTF_8), new File(cwd, "two"));
|
||||
|
||||
String[] cmd = {"tar", "--format=ustar", "-cf", "lines.tar", "one", "two"};
|
||||
String[] env = {"PATH=" + System.getenv("PATH")};
|
||||
Process pid = Runtime.getRuntime().exec(cmd, env, cwd);
|
||||
String err = CharStreams.toString(new InputStreamReader(pid.getErrorStream(), UTF_8));
|
||||
assertThat(pid.waitFor()).isEqualTo(0);
|
||||
assertThat(err.trim()).isEmpty();
|
||||
|
||||
PosixTarHeader header;
|
||||
byte[] block = new byte[512];
|
||||
try (FileInputStream input = new FileInputStream(new File(cwd, "lines.tar"))) {
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("one");
|
||||
assertThat(header.getSize()).isEqualTo(one.length());
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(one).isEqualTo(new String(block, 0, one.length(), UTF_8));
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("two");
|
||||
assertThat(header.getSize()).isEqualTo(two.length());
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(two).isEqualTo(new String(block, 0, two.length(), UTF_8));
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testReadArchiveDefaultFormat() throws Exception {
|
||||
assumeTrue(hasCommand("tar"));
|
||||
|
||||
String truth = "No one really knows\n";
|
||||
Files.write(truth.getBytes(UTF_8), folder.newFile("truth.txt"));
|
||||
|
||||
String[] cmd = {"tar", "-cf", "steam.tar", "truth.txt"};
|
||||
String[] env = {"PATH=" + System.getenv("PATH")};
|
||||
File cwd = folder.getRoot();
|
||||
Process pid = Runtime.getRuntime().exec(cmd, env, cwd);
|
||||
String err = CharStreams.toString(new InputStreamReader(pid.getErrorStream(), UTF_8));
|
||||
assertThat(pid.waitFor()).isEqualTo(0);
|
||||
assertThat(err.trim()).isEmpty();
|
||||
|
||||
PosixTarHeader header;
|
||||
byte[] block = new byte[512];
|
||||
try (FileInputStream input = new FileInputStream(new File(cwd, "steam.tar"))) {
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("truth.txt");
|
||||
assertThat(header.getSize()).isEqualTo(truth.length());
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(truth).isEqualTo(new String(block, 0, truth.length(), UTF_8));
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testCreateBigWebScaleData() throws Exception {
|
||||
assumeTrue(hasCommand("tar"));
|
||||
|
||||
String name = "rando_numberissian.mov";
|
||||
byte[] data = new byte[4 * 1024 * 1024];
|
||||
Random rand = new Random();
|
||||
rand.nextBytes(data);
|
||||
|
||||
String tarName = "occupy.tar";
|
||||
File tarFile = folder.newFile(tarName);
|
||||
try (FileOutputStream output = new FileOutputStream(tarFile)) {
|
||||
output.write(new PosixTarHeader.Builder()
|
||||
.setName(name)
|
||||
.setSize(data.length)
|
||||
.build()
|
||||
.getBytes());
|
||||
output.write(data);
|
||||
output.write(new byte[1024]);
|
||||
}
|
||||
assertThat(tarFile.length() % 512).isEqualTo(0);
|
||||
|
||||
String[] cmd = {"tar", "-xf", tarName};
|
||||
String[] env = {"PATH=" + System.getenv("PATH")};
|
||||
File cwd = folder.getRoot();
|
||||
Process pid = Runtime.getRuntime().exec(cmd, env, cwd);
|
||||
String err = CharStreams.toString(new InputStreamReader(pid.getErrorStream(), UTF_8));
|
||||
assertThat(pid.waitFor()).isEqualTo(0);
|
||||
assertThat(err.trim()).isEmpty();
|
||||
|
||||
File dataFile = new File(cwd, name);
|
||||
assertThat(dataFile.exists()).isTrue();
|
||||
assertThat(dataFile.isFile()).isTrue();
|
||||
assertThat(Files.asByteSource(dataFile).read()).isEqualTo(data);
|
||||
|
||||
Set<String> expectedFiles = ImmutableSet.of(tarName, name);
|
||||
assertThat(ImmutableSet.copyOf(folder.getRoot().list())).isEqualTo(expectedFiles);
|
||||
}
|
||||
}
|
477
util/src/test/java/google/registry/util/PosixTarHeaderTest.java
Normal file
477
util/src/test/java/google/registry/util/PosixTarHeaderTest.java
Normal file
|
@ -0,0 +1,477 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.testing.EqualsTester;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.format.ISODateTimeFormat;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link PosixTarHeader}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class PosixTarHeaderTest {
|
||||
@Test
|
||||
public void testGnuTarBlob() throws Exception {
|
||||
// This data was generated as follows:
|
||||
//
|
||||
// echo hello kitty >hello.xml
|
||||
// tar --format=ustar -cf ~/hello.tar hello.xml
|
||||
// head -c 1024 <hello.tar | base64
|
||||
//
|
||||
// As you can see, we're only going to bother with the first 1024 characters.
|
||||
byte[] gnuTarGeneratedData =
|
||||
base64()
|
||||
.decode(
|
||||
""
|
||||
+ "aGVsbG8ueG1sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA2NDAAMDU0MTI2"
|
||||
+ "NgAwMDExNjEwADAwMDAwMDAwMDE0ADEyMjAyMzEwMzI0ADAxMjQ2MQAgMAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMGphcnQAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAZW5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAw"
|
||||
+ "MDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABo"
|
||||
+ "ZWxsbyBraXR0eQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==");
|
||||
assertThat(gnuTarGeneratedData.length).isEqualTo(1024);
|
||||
|
||||
// Now we have to replicate it.
|
||||
byte[] data = "hello kitty\n".getBytes(UTF_8);
|
||||
PosixTarHeader header =
|
||||
new PosixTarHeader.Builder()
|
||||
.setType(PosixTarHeader.Type.REGULAR)
|
||||
.setName("hello.xml")
|
||||
.setSize(data.length)
|
||||
.setMode(0640)
|
||||
// This timestamp should have been midnight but I think GNU tar might not understand
|
||||
// daylight savings time. Woe is me.
|
||||
.setMtime(DateTime.parse("2013-08-13T01:50:12Z"))
|
||||
.setUname("jart")
|
||||
.setGname("eng")
|
||||
.setUid(180918) // echo $UID
|
||||
.setGid(5000) // import grp; print grp.getgrnam('eng').gr_gid
|
||||
.build();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
output.write(header.getBytes());
|
||||
output.write(data);
|
||||
// Line up with the 512-byte block boundary.
|
||||
if (data.length % 512 != 0) {
|
||||
output.write(new byte[512 - data.length % 512]);
|
||||
}
|
||||
output.write(new byte[1024]); // Bunch of null bytes to indicate end of archive.
|
||||
byte[] tarData = output.toByteArray();
|
||||
assertThat(tarData.length % 512).isEqualTo(0);
|
||||
data = Arrays.copyOf(tarData, 1024);
|
||||
|
||||
// From Wikipedia:
|
||||
// The checksum is calculated by taking the sum of the unsigned byte values of the header
|
||||
// record with the eight checksum bytes taken to be ascii spaces (decimal value 32). It is
|
||||
// stored as a six digit octal number with leading zeroes followed by a NUL and then a space.
|
||||
// Various implementations do not adhere to this format. For better compatibility, ignore
|
||||
// leading and trailing whitespace, and get the first six digits. In addition, some historic
|
||||
// tar implementations treated bytes as signed. Implementations typically calculate the
|
||||
// checksum both ways, and treat it as good if either the signed or unsigned sum matches the
|
||||
// included checksum.
|
||||
data[155] = ' ';
|
||||
|
||||
// Compare everything in the arrays except for the checksum. That way we know what's causing
|
||||
// the checksum to fail.
|
||||
byte[] gnuTarGeneratedDataNoChksum = gnuTarGeneratedData.clone();
|
||||
Arrays.fill(gnuTarGeneratedDataNoChksum, 148, 148 + 8, (byte) 0);
|
||||
byte[] dataNoChksum = data.clone();
|
||||
Arrays.fill(dataNoChksum, 148, 148 + 8, (byte) 0);
|
||||
assertThat(dataNoChksum).isEqualTo(gnuTarGeneratedDataNoChksum);
|
||||
|
||||
// Now do it again with the checksum.
|
||||
assertThat(data).isEqualTo(gnuTarGeneratedData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFields() {
|
||||
PosixTarHeader header =
|
||||
new PosixTarHeader.Builder()
|
||||
.setType(PosixTarHeader.Type.REGULAR)
|
||||
.setName("(◕‿◕).txt")
|
||||
.setSize(666)
|
||||
.setMode(0777)
|
||||
.setMtime(DateTime.parse("1984-12-18T04:20:00Z"))
|
||||
.setUname("everything i ever touched")
|
||||
.setGname("everything i ever had, has died")
|
||||
.setUid(180918)
|
||||
.setGid(5000)
|
||||
.build();
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("(◕‿◕).txt");
|
||||
assertThat(header.getSize()).isEqualTo(666);
|
||||
assertThat(header.getMode()).isEqualTo(0777);
|
||||
assertThat(header.getUname()).isEqualTo("everything i ever touched");
|
||||
assertThat(header.getGname()).isEqualTo("everything i ever had, has died");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(5000);
|
||||
assertThat(header.getMtime()).isEqualTo(DateTime.parse("1984-12-18T04:20:00Z"));
|
||||
assertThat(header.getMagic()).isEqualTo("ustar");
|
||||
assertThat(header.getVersion()).isEqualTo("00");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFieldsSomeMoar() {
|
||||
PosixTarHeader header =
|
||||
new PosixTarHeader.Builder()
|
||||
.setType(PosixTarHeader.Type.DIRECTORY)
|
||||
.setName("Black lung full of fumes, choke on memories")
|
||||
.setSize(1024 * 1024 * 1024)
|
||||
.setMode(31337)
|
||||
.setMtime(DateTime.parse("2020-12-18T04:20:00Z"))
|
||||
.setUname("every street i ever walked")
|
||||
.setGname("every home i ever had, is lost")
|
||||
.setUid(0)
|
||||
.setGid(31337)
|
||||
.build();
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.DIRECTORY);
|
||||
assertThat(header.getName()).isEqualTo("Black lung full of fumes, choke on memories");
|
||||
assertThat(header.getSize()).isEqualTo(1024 * 1024 * 1024);
|
||||
assertThat(header.getMode()).isEqualTo(31337);
|
||||
assertThat(header.getUname()).isEqualTo("every street i ever walked");
|
||||
assertThat(header.getGname()).isEqualTo("every home i ever had, is lost");
|
||||
assertThat(header.getUid()).isEqualTo(0);
|
||||
assertThat(header.getGid()).isEqualTo(31337);
|
||||
assertThat(header.getMtime()).isEqualTo(DateTime.parse("2020-12-18T04:20:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoad() {
|
||||
PosixTarHeader header =
|
||||
new PosixTarHeader.Builder()
|
||||
.setType(PosixTarHeader.Type.REGULAR)
|
||||
.setName("(◕‿◕).txt")
|
||||
.setSize(31337)
|
||||
.setMode(0777)
|
||||
.setMtime(DateTime.parse("1984-12-18T04:20:00Z"))
|
||||
.setUname("everything i ever touched")
|
||||
.setGname("everything i ever had, has died")
|
||||
.setUid(180918)
|
||||
.setGid(5000)
|
||||
.build();
|
||||
header = PosixTarHeader.from(header.getBytes()); // <-- Pay attention to this line.
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("(◕‿◕).txt");
|
||||
assertThat(header.getSize()).isEqualTo(31337);
|
||||
assertThat(header.getMode()).isEqualTo(0777);
|
||||
assertThat(header.getUname()).isEqualTo("everything i ever touched");
|
||||
assertThat(header.getGname()).isEqualTo("everything i ever had, has died");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(5000);
|
||||
assertThat(header.getMtime()).isEqualTo(DateTime.parse("1984-12-18T04:20:00Z"));
|
||||
assertThat(header.getMagic()).isEqualTo("ustar");
|
||||
assertThat(header.getVersion()).isEqualTo("00");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadChecksum() {
|
||||
PosixTarHeader header =
|
||||
new PosixTarHeader.Builder().setName("(◕‿◕).txt").setSize(31337).build();
|
||||
byte[] bytes = header.getBytes();
|
||||
bytes[150] = '0';
|
||||
bytes[151] = '0';
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> PosixTarHeader.from(bytes));
|
||||
assertThat(thrown).hasMessageThat().contains("chksum invalid");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHashEquals() {
|
||||
new EqualsTester()
|
||||
.addEqualityGroup(
|
||||
new PosixTarHeader.Builder()
|
||||
.setName("(◕‿◕).txt")
|
||||
.setSize(123)
|
||||
.setMtime(DateTime.parse("1984-12-18TZ"))
|
||||
.setUid(1234)
|
||||
.setGid(456)
|
||||
.setUname("jart")
|
||||
.setGname("wheel")
|
||||
.build(),
|
||||
new PosixTarHeader.Builder()
|
||||
.setName("(◕‿◕).txt")
|
||||
.setSize(123)
|
||||
.setMtime(DateTime.parse("1984-12-18TZ"))
|
||||
.setUid(1234)
|
||||
.setGid(456)
|
||||
.setUname("jart")
|
||||
.setGname("wheel")
|
||||
.build())
|
||||
.addEqualityGroup(
|
||||
new PosixTarHeader.Builder()
|
||||
.setName("(•︵•).txt") // Awwwww! It looks so sad...
|
||||
.setSize(123)
|
||||
.build())
|
||||
.testEquals();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBsdTarFormatUstar() throws Exception {
|
||||
// $ tar --version
|
||||
// bsdtar 2.8.3 - libarchive 2.8.3
|
||||
|
||||
// echo no rain can wash away my tears >liketears
|
||||
// echo no wind can soothe my pain >inrain
|
||||
// chmod 0600 liketears inrain
|
||||
// tar --format=ustar -c liketears inrain | gzip | base64
|
||||
InputStream input =
|
||||
new GZIPInputStream(
|
||||
new ByteArrayInputStream(
|
||||
base64()
|
||||
.decode(
|
||||
"H4sIAM17DVIAA+3T0QqCMBTGca97ivMIx03n84waaNkMNcS3T4OCbuymFcH/dzc22Dd2vrY5h"
|
||||
+ "TH4fsjSUVWnKllZ5MY5Wde6rvXBVpIbo9ZUpnKFaG7VlZlowkxP12H0/RLl6Ptx69xUh"
|
||||
+ "9Bu7L8+Sj4bMp3YSe+bKHsfZfJDLX7ys5xnuQ/F7tfxkFgT1+9Pe8f7/ttn/12hS/+NL"
|
||||
+ "Sr6/w1L/6cmHu79H7purMNa/ssyE3QfAAAAAAAAAAAAAADgH9wAqAJg4gAoAAA=")));
|
||||
|
||||
PosixTarHeader header;
|
||||
byte[] block = new byte[512];
|
||||
String likeTears = "no rain can wash away my tears\n";
|
||||
String inRain = "no wind can soothe my pain\n";
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getName()).isEqualTo("liketears");
|
||||
assertThat(header.getSize()).isEqualTo(likeTears.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("wheel");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(0);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getName()).isEqualTo("inrain");
|
||||
assertThat(header.getSize()).isEqualTo(inRain.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("wheel");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(0);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBsdTarFormatDefault() throws Exception {
|
||||
// $ tar --version
|
||||
// bsdtar 2.8.3 - libarchive 2.8.3
|
||||
|
||||
// echo no rain can wash away my tears >liketears
|
||||
// echo no wind can soothe my pain >inrain
|
||||
// chmod 0600 liketears inrain
|
||||
// tar -c liketears inrain | gzip | base64
|
||||
InputStream input =
|
||||
new GZIPInputStream(
|
||||
new ByteArrayInputStream(
|
||||
base64()
|
||||
.decode(
|
||||
"H4sIAM17DVIAA+3T0QqCMBTGca97ivMIx03n84waaNkMNcS3T4OCbuymFcH/dzc22Dd2vrY5h"
|
||||
+ "TH4fsjSUVWnKllZ5MY5Wde6rvXBVpIbo9ZUpnKFaG7VlZlowkxP12H0/RLl6Ptx69xUh"
|
||||
+ "9Bu7L8+Sj4bMp3YSe+bKHsfZfJDLX7ys5xnuQ/F7tfxkFgT1+9Pe8f7/ttn/12hS/+NL"
|
||||
+ "Sr6/w1L/6cmHu79H7purMNa/ssyE3QfAAAAAAAAAAAAAADgH9wAqAJg4gAoAAA=")));
|
||||
|
||||
PosixTarHeader header;
|
||||
byte[] block = new byte[512];
|
||||
String likeTears = "no rain can wash away my tears\n";
|
||||
String inRain = "no wind can soothe my pain\n";
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getName()).isEqualTo("liketears");
|
||||
assertThat(header.getSize()).isEqualTo(likeTears.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("wheel");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(0);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getName()).isEqualTo("inrain");
|
||||
assertThat(header.getSize()).isEqualTo(inRain.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("wheel");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(0);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadGnuTarFormatDefault() throws Exception {
|
||||
// $ tar --version
|
||||
// tar (GNU tar) 1.26
|
||||
|
||||
// echo no rain can wash away my tears >liketears
|
||||
// echo no wind can soothe my pain >inrain
|
||||
// chmod 0600 liketears inrain
|
||||
// tar -c liketears inrain | gzip | base64
|
||||
InputStream input =
|
||||
new GZIPInputStream(
|
||||
new ByteArrayInputStream(
|
||||
base64()
|
||||
.decode(
|
||||
"H4sIAOB8DVIAA+3TTQ6DIBCGYdY9BUcYUPE8pDWV/mCjNsbbF01jurIr25i8z4ZACAxhvlu4V"
|
||||
+ "n3l205tRxInoqTIjXUuzY1xRub1WVYqY61ktnSmyJUYWzhRWjasafHset+mUi6+7df2V"
|
||||
+ "fG8es77Kcu4E7HRrQ9RH33Ug+9q7Qc/6vuo56Y4/Ls8bCzE6fu3veN7/rOP/Lsp/1Jk5"
|
||||
+ "P8XUv6HEE9z/rum6etqCv8j9QTZBwAAAAAAAAAAAAAA2IMXm3pYMgAoAAA=")));
|
||||
|
||||
PosixTarHeader header;
|
||||
byte[] block = new byte[512];
|
||||
String likeTears = "no rain can wash away my tears\n";
|
||||
String inRain = "no wind can soothe my pain\n";
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getName()).isEqualTo("liketears");
|
||||
assertThat(header.getSize()).isEqualTo(likeTears.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("eng");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(5000);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getName()).isEqualTo("inrain");
|
||||
assertThat(header.getSize()).isEqualTo(inRain.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("eng");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(5000);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadGnuTarFormatUstar() throws Exception {
|
||||
// $ tar --version
|
||||
// tar (GNU tar) 1.26
|
||||
|
||||
// echo no rain can wash away my tears >liketears
|
||||
// echo no wind can soothe my pain >inrain
|
||||
// chmod 0600 liketears inrain
|
||||
// tar --format=ustar -c liketears inrain | gzip | base64
|
||||
InputStream input =
|
||||
new GZIPInputStream(
|
||||
new ByteArrayInputStream(
|
||||
base64()
|
||||
.decode(
|
||||
"H4sIAOB8DVIAA+3TTQ6DIBCGYdY9BUcYUPE8pDWV/mCjNsbbF01jurIr25i8z4ZACAxhvlu4V"
|
||||
+ "n3l205tRxInoqTIjXUuzY1xRub1WVYqY61ktnSmyJUYWzhRWjasafHset+mUi6+7df2V"
|
||||
+ "fG8es77Kcu4E7HRrQ9RH33Ug+9q7Qc/6vuo56Y4/Ls8bCzE6fu3veN7/rOP/Lsp/1Jk5"
|
||||
+ "P8XUv6HEE9z/rum6etqCv8j9QTZBwAAAAAAAAAAAAAA2IMXm3pYMgAoAAA=")));
|
||||
|
||||
PosixTarHeader header;
|
||||
byte[] block = new byte[512];
|
||||
String likeTears = "no rain can wash away my tears\n";
|
||||
String inRain = "no wind can soothe my pain\n";
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("liketears");
|
||||
assertThat(header.getSize()).isEqualTo(likeTears.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("eng");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(5000);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
header = PosixTarHeader.from(block);
|
||||
assertThat(header.getType()).isEqualTo(PosixTarHeader.Type.REGULAR);
|
||||
assertThat(header.getName()).isEqualTo("inrain");
|
||||
assertThat(header.getSize()).isEqualTo(inRain.length());
|
||||
assertThat(header.getMode()).isEqualTo(0600);
|
||||
assertThat(header.getUname()).isEqualTo("jart");
|
||||
assertThat(header.getGname()).isEqualTo("eng");
|
||||
assertThat(header.getUid()).isEqualTo(180918);
|
||||
assertThat(header.getGid()).isEqualTo(5000);
|
||||
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
|
||||
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
assertThat(input.read(block)).isEqualTo(512);
|
||||
assertWithMessage("End of archive marker corrupt").that(block).isEqualTo(new byte[512]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link RegistrarUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class RegistrarUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testNormalizeRegistrarName_letterOrDigitOnly() {
|
||||
assertThat(RegistrarUtils.normalizeRegistrarName("129abzAZ")).isEqualTo("129abzaz");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeRegistrarName_hasSymbols() {
|
||||
assertThat(RegistrarUtils.normalizeRegistrarName("^}129a(bzAZ/:")).isEqualTo("129abzaz");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.LogsSubject.assertAboutLogs;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.appengine.api.log.LogQuery;
|
||||
import com.google.appengine.api.log.LogService;
|
||||
import com.google.appengine.api.log.RequestLogs;
|
||||
import com.google.apphosting.api.ApiProxy;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.flogger.LoggerConfig;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.util.logging.Level;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link RequestStatusCheckerImpl}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class RequestStatusCheckerImplTest {
|
||||
|
||||
private static final TestLogHandler logHandler = new TestLogHandler();
|
||||
|
||||
private static final RequestStatusChecker requestStatusChecker = new RequestStatusCheckerImpl();
|
||||
|
||||
/**
|
||||
* Matcher for the expected LogQuery in {@link RequestStatusCheckerImpl#isRunning}.
|
||||
*
|
||||
* Because LogQuery doesn't have a .equals function, we have to create an actual matcher to make
|
||||
* sure we have the right argument in our mocks.
|
||||
*/
|
||||
private static LogQuery expectedLogQuery(final String requestLogId) {
|
||||
return argThat(
|
||||
object -> {
|
||||
assertThat(object).isInstanceOf(LogQuery.class);
|
||||
assertThat(object.getRequestIds()).containsExactly(requestLogId);
|
||||
assertThat(object.getIncludeAppLogs()).isFalse();
|
||||
assertThat(object.getIncludeIncomplete()).isTrue();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Rule
|
||||
public AppEngineRule appEngineRule = AppEngineRule.builder().build();
|
||||
|
||||
@Before public void setUp() {
|
||||
LoggerConfig.getConfig(RequestStatusCheckerImpl.class).addHandler(logHandler);
|
||||
RequestStatusCheckerImpl.logService = mock(LogService.class);
|
||||
}
|
||||
|
||||
@After public void tearDown() {
|
||||
LoggerConfig.getConfig(RequestStatusCheckerImpl.class).removeHandler(logHandler);
|
||||
}
|
||||
|
||||
// If a logId is unrecognized, it could be that the log hasn't been uploaded yet - so we assume
|
||||
// it's a request that has just started running recently.
|
||||
@Test public void testIsRunning_unrecognized() {
|
||||
when(RequestStatusCheckerImpl.logService.fetch(expectedLogQuery("12345678")))
|
||||
.thenReturn(ImmutableList.of());
|
||||
assertThat(requestStatusChecker.isRunning("12345678")).isTrue();
|
||||
assertAboutLogs()
|
||||
.that(logHandler)
|
||||
.hasLogAtLevelWithMessage(Level.INFO, "Queried an unrecognized requestLogId");
|
||||
}
|
||||
|
||||
@Test public void testIsRunning_notFinished() {
|
||||
RequestLogs requestLogs = new RequestLogs();
|
||||
requestLogs.setFinished(false);
|
||||
|
||||
when(RequestStatusCheckerImpl.logService.fetch(expectedLogQuery("12345678")))
|
||||
.thenReturn(ImmutableList.of(requestLogs));
|
||||
|
||||
assertThat(requestStatusChecker.isRunning("12345678")).isTrue();
|
||||
assertAboutLogs()
|
||||
.that(logHandler)
|
||||
.hasLogAtLevelWithMessage(Level.INFO, "isFinished: false");
|
||||
}
|
||||
|
||||
@Test public void testIsRunning_finished() {
|
||||
RequestLogs requestLogs = new RequestLogs();
|
||||
requestLogs.setFinished(true);
|
||||
|
||||
when(RequestStatusCheckerImpl.logService.fetch(expectedLogQuery("12345678")))
|
||||
.thenReturn(ImmutableList.of(requestLogs));
|
||||
|
||||
assertThat(requestStatusChecker.isRunning("12345678")).isFalse();
|
||||
assertAboutLogs()
|
||||
.that(logHandler)
|
||||
.hasLogAtLevelWithMessage(Level.INFO, "isFinished: true");
|
||||
}
|
||||
|
||||
@Test public void testGetLogId_returnsRequestLogId() {
|
||||
String expectedLogId = ApiProxy.getCurrentEnvironment().getAttributes().get(
|
||||
"com.google.appengine.runtime.request_log_id").toString();
|
||||
assertThat(requestStatusChecker.getLogId()).isEqualTo(expectedLogId);
|
||||
}
|
||||
|
||||
@Test public void testGetLogId_createsLog() {
|
||||
requestStatusChecker.getLogId();
|
||||
assertAboutLogs()
|
||||
.that(logHandler)
|
||||
.hasLogAtLevelWithMessage(Level.INFO, "Current requestLogId: ");
|
||||
}
|
||||
}
|
128
util/src/test/java/google/registry/util/RetrierTest.java
Normal file
128
util/src/test/java/google/registry/util/RetrierTest.java
Normal file
|
@ -0,0 +1,128 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.util.Retrier.FailureReporter;
|
||||
import java.util.concurrent.Callable;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link Retrier}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class RetrierTest {
|
||||
|
||||
Retrier retrier = new Retrier(new FakeSleeper(new FakeClock()), 3);
|
||||
|
||||
/** An exception to throw from {@link CountingThrower}. */
|
||||
static class CountingException extends RuntimeException {
|
||||
CountingException(int count) {
|
||||
super("" + count);
|
||||
}
|
||||
}
|
||||
|
||||
/** Test object that always throws an exception with the current count. */
|
||||
static class CountingThrower implements Callable<Integer> {
|
||||
|
||||
int count = 0;
|
||||
|
||||
final int numThrows;
|
||||
|
||||
CountingThrower(int numThrows) {
|
||||
this.numThrows = numThrows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
if (count == numThrows) {
|
||||
return numThrows;
|
||||
}
|
||||
count++;
|
||||
throw new CountingException(count);
|
||||
}
|
||||
}
|
||||
|
||||
static class TestReporter implements FailureReporter {
|
||||
int numBeforeRetry = 0;
|
||||
|
||||
@Override
|
||||
public void beforeRetry(Throwable e, int failures, int maxAttempts) {
|
||||
numBeforeRetry++;
|
||||
assertThat(failures).isEqualTo(numBeforeRetry);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryableException() {
|
||||
CountingException thrown =
|
||||
assertThrows(
|
||||
CountingException.class,
|
||||
() -> retrier.callWithRetry(new CountingThrower(3), CountingException.class));
|
||||
assertThat(thrown).hasMessageThat().contains("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnretryableException() {
|
||||
CountingException thrown =
|
||||
assertThrows(
|
||||
CountingException.class,
|
||||
() -> retrier.callWithRetry(new CountingThrower(5), IllegalArgumentException.class));
|
||||
assertThat(thrown).hasMessageThat().contains("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetrySucceeded() {
|
||||
assertThat(retrier.callWithRetry(new CountingThrower(2), CountingException.class))
|
||||
.isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryFailed_withReporter() {
|
||||
CountingException thrown =
|
||||
assertThrows(
|
||||
CountingException.class,
|
||||
() -> {
|
||||
TestReporter reporter = new TestReporter();
|
||||
try {
|
||||
retrier.callWithRetry(new CountingThrower(3), reporter, CountingException.class);
|
||||
} catch (CountingException expected) {
|
||||
assertThat(reporter.numBeforeRetry).isEqualTo(2);
|
||||
throw expected;
|
||||
}
|
||||
});
|
||||
assertThat(thrown).hasMessageThat().contains("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetrySucceeded_withReporter() {
|
||||
TestReporter reporter = new TestReporter();
|
||||
assertThat(retrier.callWithRetry(new CountingThrower(2), reporter, CountingException.class))
|
||||
.isEqualTo(2);
|
||||
assertThat(reporter.numBeforeRetry).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstTrySucceeded_withReporter() {
|
||||
TestReporter reporter = new TestReporter();
|
||||
assertThat(retrier.callWithRetry(new CountingThrower(0), reporter, CountingException.class))
|
||||
.isEqualTo(0);
|
||||
assertThat(reporter.numBeforeRetry).isEqualTo(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import google.registry.util.EmailMessage.Attachment;
|
||||
import javax.mail.BodyPart;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.Message.RecipientType;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Unit tests for {@link SendEmailService}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class SendEmailServiceTest {
|
||||
|
||||
@Rule
|
||||
public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
private final Retrier retrier = new Retrier(new FakeSleeper(new FakeClock()), 2);
|
||||
private final TransportEmailSender wrapper = mock(TransportEmailSender.class);
|
||||
private final SendEmailService sendEmailService = new SendEmailService(retrier, wrapper);
|
||||
|
||||
@Captor private ArgumentCaptor<Message> messageCaptor;
|
||||
|
||||
@Test
|
||||
public void testSuccess_simple() throws Exception {
|
||||
EmailMessage content = createBuilder().build();
|
||||
sendEmailService.sendEmail(content);
|
||||
Message message = getMessage();
|
||||
assertThat(message.getAllRecipients())
|
||||
.asList()
|
||||
.containsExactly(new InternetAddress("fake@example.com"));
|
||||
assertThat(message.getFrom())
|
||||
.asList()
|
||||
.containsExactly(new InternetAddress("registry@example.com"));
|
||||
assertThat(message.getRecipients(RecipientType.BCC)).isNull();
|
||||
assertThat(message.getSubject()).isEqualTo("Subject");
|
||||
assertThat(message.getContentType()).startsWith("multipart/mixed");
|
||||
assertThat(getInternalContent(message).getContent().toString()).isEqualTo("body");
|
||||
assertThat(getInternalContent(message).getContentType()).isEqualTo("text/plain; charset=utf-8");
|
||||
assertThat(((MimeMultipart) message.getContent()).getCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_bcc() throws Exception {
|
||||
EmailMessage content = createBuilder().setBcc(new InternetAddress("bcc@example.com")).build();
|
||||
sendEmailService.sendEmail(content);
|
||||
Message message = getMessage();
|
||||
assertThat(message.getRecipients(RecipientType.BCC))
|
||||
.asList()
|
||||
.containsExactly(new InternetAddress("bcc@example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_contentType() throws Exception {
|
||||
EmailMessage content = createBuilder().setContentType(MediaType.HTML_UTF_8).build();
|
||||
sendEmailService.sendEmail(content);
|
||||
Message message = getMessage();
|
||||
assertThat(getInternalContent(message).getContentType())
|
||||
.isEqualTo("text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_attachment() throws Exception {
|
||||
EmailMessage content =
|
||||
createBuilder()
|
||||
.setAttachment(
|
||||
Attachment.newBuilder()
|
||||
.setFilename("filename")
|
||||
.setContent("foo,bar\nbaz,qux")
|
||||
.setContentType(MediaType.CSV_UTF_8)
|
||||
.build())
|
||||
.build();
|
||||
sendEmailService.sendEmail(content);
|
||||
Message message = getMessage();
|
||||
assertThat(((MimeMultipart) message.getContent()).getCount()).isEqualTo(2);
|
||||
BodyPart attachment = ((MimeMultipart) message.getContent()).getBodyPart(1);
|
||||
assertThat(attachment.getContent()).isEqualTo("foo,bar\nbaz,qux");
|
||||
assertThat(attachment.getContentType()).endsWith("name=filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_retry() throws Exception {
|
||||
doThrow(new MessagingException("hi"))
|
||||
.doNothing()
|
||||
.when(wrapper)
|
||||
.sendMessage(messageCaptor.capture());
|
||||
EmailMessage content = createBuilder().build();
|
||||
sendEmailService.sendEmail(content);
|
||||
assertThat(messageCaptor.getValue().getSubject()).isEqualTo("Subject");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_wrongExceptionType() throws Exception {
|
||||
doThrow(new RuntimeException("this is a runtime exception")).when(wrapper).sendMessage(any());
|
||||
EmailMessage content = createBuilder().build();
|
||||
RuntimeException thrown =
|
||||
assertThrows(RuntimeException.class, () -> sendEmailService.sendEmail(content));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("this is a runtime exception");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_tooManyTries() throws Exception {
|
||||
doThrow(new MessagingException("hi"))
|
||||
.doThrow(new MessagingException("second"))
|
||||
.when(wrapper)
|
||||
.sendMessage(any());
|
||||
EmailMessage content = createBuilder().build();
|
||||
RuntimeException thrown =
|
||||
assertThrows(RuntimeException.class, () -> sendEmailService.sendEmail(content));
|
||||
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("second");
|
||||
assertThat(thrown).hasCauseThat().isInstanceOf(MessagingException.class);
|
||||
}
|
||||
|
||||
private EmailMessage.Builder createBuilder() throws Exception {
|
||||
return EmailMessage.newBuilder()
|
||||
.setFrom(new InternetAddress("registry@example.com"))
|
||||
.addRecipient(new InternetAddress("fake@example.com"))
|
||||
.setSubject("Subject")
|
||||
.setBody("body");
|
||||
}
|
||||
|
||||
private Message getMessage() throws MessagingException {
|
||||
verify(wrapper).sendMessage(messageCaptor.capture());
|
||||
return messageCaptor.getValue();
|
||||
}
|
||||
|
||||
private BodyPart getInternalContent(Message message) throws Exception {
|
||||
return ((MimeMultipart) message.getContent()).getBodyPart(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.SerializeUtils.deserialize;
|
||||
import static google.registry.util.SerializeUtils.serialize;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link SerializeUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class SerializeUtilsTest {
|
||||
|
||||
static class Lol {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LOL_VALUE";
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testSerialize_nullValue_returnsNull() {
|
||||
assertThat(serialize(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserialize_nullValue_returnsNull() {
|
||||
assertThat(deserialize(Object.class, null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserialize_stringValue_maintainsValue() {
|
||||
assertThat(deserialize(String.class, serialize("hello"))).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerialize_objectDoesntImplementSerialize_hasInformativeError() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> serialize(new Lol()));
|
||||
assertThat(thrown).hasMessageThat().contains("Unable to serialize: LOL_VALUE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserialize_badValue_hasInformativeError() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> deserialize(String.class, new byte[] {(byte) 0xff}));
|
||||
assertThat(thrown).hasMessageThat().contains("Unable to deserialize: objectBytes=FF");
|
||||
}
|
||||
}
|
146
util/src/test/java/google/registry/util/SqlTemplateTest.java
Normal file
146
util/src/test/java/google/registry/util/SqlTemplateTest.java
Normal file
|
@ -0,0 +1,146 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link SqlTemplate}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class SqlTemplateTest {
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate() {
|
||||
assertThat(
|
||||
SqlTemplate.create("%TEST%")
|
||||
.put("TEST", "hello world")
|
||||
.build())
|
||||
.isEqualTo("hello world");
|
||||
assertThat(
|
||||
SqlTemplate.create("one %TWO% three")
|
||||
.put("TWO", "2")
|
||||
.build())
|
||||
.isEqualTo("one 2 three");
|
||||
assertThat(
|
||||
SqlTemplate.create("%ONE% %TWO% %THREE%")
|
||||
.put("ONE", "1")
|
||||
.put("TWO", "2")
|
||||
.put("THREE", "3")
|
||||
.build())
|
||||
.isEqualTo("1 2 3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_substitutionButNoVariables() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> SqlTemplate.create("").put("ONE", "1").build());
|
||||
assertThat(thrown).hasMessageThat().contains("Not found in template: ONE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_substitutionButMissingVariables() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("%ONE%").put("ONE", "1").put("TWO", "2").build());
|
||||
assertThat(thrown).hasMessageThat().contains("Not found in template: TWO");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_sameKeyTwice_failsEarly() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("%ONE%").put("ONE", "1").put("ONE", "2"));
|
||||
assertThat(thrown).hasMessageThat().contains("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_variablesButNotEnoughSubstitutions() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("%ONE% %TWO%").put("ONE", "1").build());
|
||||
assertThat(thrown).hasMessageThat().contains("%TWO% found in template but no substitution");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_mismatchedVariableAndSubstitution() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("%TWO%").put("TOO", "2").build());
|
||||
assertThat(thrown).hasMessageThat().contains("%TWO% found in template but no substitution");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_missingKeyVals_whatsThePoint() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> SqlTemplate.create("%TWO%").build());
|
||||
assertThat(thrown).hasMessageThat().contains("%TWO% found in template but no substitution");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_lowercaseKey_notAllowed() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("%test%").put("test", "hello world").build());
|
||||
assertThat(thrown).hasMessageThat().contains("Bad substitution key: test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_substitution_disallowsSingleQuotes() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("The words are '%LOS%' and baz").put("LOS", "foo'bar"));
|
||||
assertThat(thrown).hasMessageThat().contains("Illegal characters in foo'bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_substitution_disallowsDoubleQuotes() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> SqlTemplate.create("The words are '%LOS%' and baz").put("LOS", "foo\"bar"));
|
||||
assertThat(thrown).hasMessageThat().contains("Illegal characters in foo\"bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_quoteMismatch_throwsError() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
SqlTemplate.create("The words are \"%LOS%' and baz").put("LOS", "foobar").build());
|
||||
assertThat(thrown).hasMessageThat().contains("Quote mismatch: \"%LOS%'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFillSqlTemplate_extendedQuote_throwsError() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
SqlTemplate.create("The words are '%LOS%-lol' and baz").put("LOS", "roid").build());
|
||||
assertThat(thrown).hasMessageThat().contains("Quote mismatch: '%LOS%");
|
||||
}
|
||||
}
|
145
util/src/test/java/google/registry/util/TaskQueueUtilsTest.java
Normal file
145
util/src/test/java/google/registry/util/TaskQueueUtilsTest.java
Normal file
|
@ -0,0 +1,145 @@
|
|||
// Copyright 2018 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.util;
|
||||
|
||||
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.testing.TaskQueueHelper.getQueueInfo;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.appengine.api.taskqueue.Queue;
|
||||
import com.google.appengine.api.taskqueue.QueueFactory;
|
||||
import com.google.appengine.api.taskqueue.TaskHandle;
|
||||
import com.google.appengine.api.taskqueue.TaskOptions;
|
||||
import com.google.appengine.api.taskqueue.TransientFailureException;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeSleeper;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link TaskQueueUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public final class TaskQueueUtilsTest {
|
||||
|
||||
private static final int MAX_RETRIES = 3;
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine =
|
||||
AppEngineRule.builder().withDatastore().withTaskQueue().build();
|
||||
|
||||
private int origBatchSize;
|
||||
|
||||
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
|
||||
private final FakeSleeper sleeper = new FakeSleeper(clock);
|
||||
private final TaskQueueUtils taskQueueUtils =
|
||||
new TaskQueueUtils(new Retrier(sleeper, MAX_RETRIES));
|
||||
private final Queue queue = mock(Queue.class);
|
||||
private final TaskOptions task = withUrl("url").taskName("name");
|
||||
private final TaskHandle handle = new TaskHandle(task, "handle");
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
origBatchSize = TaskQueueUtils.BATCH_SIZE;
|
||||
TaskQueueUtils.BATCH_SIZE = 2;
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
TaskQueueUtils.BATCH_SIZE = origBatchSize;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnqueue_worksOnFirstTry_doesntSleep() {
|
||||
when(queue.add(ImmutableList.of(task))).thenReturn(ImmutableList.of(handle));
|
||||
assertThat(taskQueueUtils.enqueue(queue, task)).isSameInstanceAs(handle);
|
||||
verify(queue).add(ImmutableList.of(task));
|
||||
assertThat(clock.nowUtc()).isEqualTo(DateTime.parse("2000-01-01TZ"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnqueue_twoTransientErrorsThenSuccess_stillWorksAfterSleeping() {
|
||||
when(queue.add(ImmutableList.of(task)))
|
||||
.thenThrow(new TransientFailureException(""))
|
||||
.thenThrow(new TransientFailureException(""))
|
||||
.thenReturn(ImmutableList.of(handle));
|
||||
assertThat(taskQueueUtils.enqueue(queue, task)).isSameInstanceAs(handle);
|
||||
verify(queue, times(3)).add(ImmutableList.of(task));
|
||||
assertThat(clock.nowUtc()).isEqualTo(DateTime.parse("2000-01-01T00:00:00.6Z")); // 200 + 400ms
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnqueue_multiple() {
|
||||
TaskOptions taskA = withUrl("a").taskName("a");
|
||||
TaskOptions taskB = withUrl("b").taskName("b");
|
||||
ImmutableList<TaskHandle> handles =
|
||||
ImmutableList.of(new TaskHandle(taskA, "a"), new TaskHandle(taskB, "b"));
|
||||
when(queue.add(ImmutableList.of(taskA, taskB))).thenReturn(handles);
|
||||
assertThat(taskQueueUtils.enqueue(queue, ImmutableList.of(taskA, taskB)))
|
||||
.isSameInstanceAs(handles);
|
||||
assertThat(clock.nowUtc()).isEqualTo(DateTime.parse("2000-01-01TZ"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnqueue_maxRetries_givesUp() {
|
||||
when(queue.add(ImmutableList.of(task)))
|
||||
.thenThrow(new TransientFailureException("one"))
|
||||
.thenThrow(new TransientFailureException("two"))
|
||||
.thenThrow(new TransientFailureException("three"))
|
||||
.thenThrow(new TransientFailureException("four"));
|
||||
TransientFailureException thrown =
|
||||
assertThrows(TransientFailureException.class, () -> taskQueueUtils.enqueue(queue, task));
|
||||
assertThat(thrown).hasMessageThat().contains("three");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnqueue_transientErrorThenInterrupt_throwsTransientError() {
|
||||
when(queue.add(ImmutableList.of(task))).thenThrow(new TransientFailureException(""));
|
||||
try {
|
||||
Thread.currentThread().interrupt();
|
||||
assertThrows(TransientFailureException.class, () -> taskQueueUtils.enqueue(queue, task));
|
||||
} finally {
|
||||
Thread.interrupted(); // Clear interrupt state so it doesn't pwn other tests.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteTasks_usesMultipleBatches() {
|
||||
Queue defaultQ = QueueFactory.getQueue("default");
|
||||
TaskOptions taskOptA = withUrl("/a").taskName("a");
|
||||
TaskOptions taskOptB = withUrl("/b").taskName("b");
|
||||
TaskOptions taskOptC = withUrl("/c").taskName("c");
|
||||
taskQueueUtils.enqueue(defaultQ, ImmutableList.of(taskOptA, taskOptB, taskOptC));
|
||||
assertThat(getQueueInfo("default").getTaskInfo()).hasSize(3);
|
||||
|
||||
taskQueueUtils.deleteTasks(
|
||||
defaultQ,
|
||||
ImmutableList.of(
|
||||
new TaskHandle(taskOptA, "default"),
|
||||
new TaskHandle(taskOptB, "default"),
|
||||
new TaskHandle(taskOptC, "default")));
|
||||
assertThat(getQueueInfo("default").getTaskInfo()).hasSize(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link TeeOutputStream}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class TeeOutputStreamTest {
|
||||
private final ByteArrayOutputStream outputA = new ByteArrayOutputStream();
|
||||
private final ByteArrayOutputStream outputB = new ByteArrayOutputStream();
|
||||
private final ByteArrayOutputStream outputC = new ByteArrayOutputStream();
|
||||
|
||||
@Test
|
||||
public void testWrite_writesToMultipleStreams() throws Exception {
|
||||
// Write shared data using the tee output stream.
|
||||
try (OutputStream tee =
|
||||
new TeeOutputStream(asList(outputA, outputB, outputC))) {
|
||||
tee.write("hello ".getBytes(UTF_8));
|
||||
tee.write("hello world!".getBytes(UTF_8), 6, 5);
|
||||
tee.write('!');
|
||||
}
|
||||
// Write some more data to the different streams - they should not have been closed.
|
||||
outputA.write("a".getBytes(UTF_8));
|
||||
outputB.write("b".getBytes(UTF_8));
|
||||
outputC.write("c".getBytes(UTF_8));
|
||||
// Check the results.
|
||||
assertThat(outputA.toString()).isEqualTo("hello world!a");
|
||||
assertThat(outputB.toString()).isEqualTo("hello world!b");
|
||||
assertThat(outputC.toString()).isEqualTo("hello world!c");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testConstructor_failsWithEmptyIterable() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new TeeOutputStream(ImmutableSet.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteInteger_failsAfterClose() throws Exception {
|
||||
OutputStream tee = new TeeOutputStream(ImmutableList.of(outputA));
|
||||
tee.close();
|
||||
IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> tee.write(1));
|
||||
assertThat(thrown).hasMessageThat().contains("outputstream closed");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteByteArray_failsAfterClose() throws Exception {
|
||||
OutputStream tee = new TeeOutputStream(ImmutableList.of(outputA));
|
||||
tee.close();
|
||||
IllegalStateException thrown =
|
||||
assertThrows(IllegalStateException.class, () -> tee.write("hello".getBytes(UTF_8)));
|
||||
assertThat(thrown).hasMessageThat().contains("outputstream closed");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteByteSubarray_failsAfterClose() throws Exception {
|
||||
OutputStream tee = new TeeOutputStream(ImmutableList.of(outputA));
|
||||
tee.close();
|
||||
IllegalStateException thrown =
|
||||
assertThrows(IllegalStateException.class, () -> tee.write("hello".getBytes(UTF_8), 1, 3));
|
||||
assertThat(thrown).hasMessageThat().contains("outputstream closed");
|
||||
}
|
||||
}
|
73
util/src/test/java/google/registry/util/TypeUtilsTest.java
Normal file
73
util/src/test/java/google/registry/util/TypeUtilsTest.java
Normal file
|
@ -0,0 +1,73 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link TypeUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class TypeUtilsTest {
|
||||
@Test
|
||||
public void test_getClassFromString_validClass() {
|
||||
Class<? extends Serializable> clazz =
|
||||
TypeUtils.getClassFromString("java.util.ArrayList", Serializable.class);
|
||||
assertThat(clazz).isEqualTo(ArrayList.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getClassFromString_notAssignableFrom() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> TypeUtils.getClassFromString("java.util.ArrayList", Integer.class));
|
||||
assertThat(thrown).hasMessageThat().contains("ArrayList does not implement/extend Integer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getClassFromString_unknownClass() {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> TypeUtils.getClassFromString("com.fake.company.nonexistent.Class", Object.class));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Failed to load class com.fake.company.nonexistent.Class");
|
||||
}
|
||||
|
||||
public static class ExampleClass {
|
||||
String val;
|
||||
|
||||
public ExampleClass(String val) {
|
||||
this.val = val;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_instantiateWithArg() {
|
||||
Class<ExampleClass> clazz =
|
||||
TypeUtils.getClassFromString(
|
||||
"google.registry.util.TypeUtilsTest$ExampleClass", ExampleClass.class);
|
||||
|
||||
ExampleClass result = TypeUtils.instantiate(clazz, "test");
|
||||
assertThat(result.val).isEqualTo("test");
|
||||
}
|
||||
}
|
109
util/src/test/java/google/registry/util/UrlFetchUtilsTest.java
Normal file
109
util/src/test/java/google/registry/util/UrlFetchUtilsTest.java
Normal file
|
@ -0,0 +1,109 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
|
||||
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
|
||||
import static com.google.common.net.MediaType.CSV_UTF_8;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.UrlFetchUtils.setPayloadMultipart;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import com.google.appengine.api.urlfetch.HTTPHeader;
|
||||
import com.google.appengine.api.urlfetch.HTTPRequest;
|
||||
import google.registry.testing.AppEngineRule;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit tests for {@link UrlFetchUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class UrlFetchUtilsTest {
|
||||
|
||||
@Rule
|
||||
public final AppEngineRule appEngine = AppEngineRule.builder()
|
||||
.build();
|
||||
|
||||
private final Random random = mock(Random.class);
|
||||
|
||||
@Before
|
||||
public void setupRandomZeroes() {
|
||||
doAnswer(
|
||||
info -> {
|
||||
Arrays.fill((byte[]) info.getArguments()[0], (byte) 0);
|
||||
return null;
|
||||
})
|
||||
.when(random)
|
||||
.nextBytes(any(byte[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPayloadMultipart() {
|
||||
HTTPRequest request = mock(HTTPRequest.class);
|
||||
setPayloadMultipart(
|
||||
request,
|
||||
"lol",
|
||||
"cat",
|
||||
CSV_UTF_8,
|
||||
"The nice people at the store say hello. ヘ(◕。◕ヘ)",
|
||||
random);
|
||||
ArgumentCaptor<HTTPHeader> headerCaptor = ArgumentCaptor.forClass(HTTPHeader.class);
|
||||
verify(request, times(2)).addHeader(headerCaptor.capture());
|
||||
List<HTTPHeader> addedHeaders = headerCaptor.getAllValues();
|
||||
assertThat(addedHeaders.get(0).getName()).isEqualTo(CONTENT_TYPE);
|
||||
assertThat(addedHeaders.get(0).getValue())
|
||||
.isEqualTo(
|
||||
"multipart/form-data; "
|
||||
+ "boundary=\"------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"");
|
||||
assertThat(addedHeaders.get(1).getName()).isEqualTo(CONTENT_LENGTH);
|
||||
assertThat(addedHeaders.get(1).getValue()).isEqualTo("294");
|
||||
String payload = "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"
|
||||
+ "Content-Disposition: form-data; name=\"lol\"; filename=\"cat\"\r\n"
|
||||
+ "Content-Type: text/csv; charset=utf-8\r\n"
|
||||
+ "\r\n"
|
||||
+ "The nice people at the store say hello. ヘ(◕。◕ヘ)\r\n"
|
||||
+ "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--\r\n";
|
||||
verify(request).setPayload(payload.getBytes(UTF_8));
|
||||
verifyNoMoreInteractions(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPayloadMultipart_boundaryInPayload() {
|
||||
HTTPRequest request = mock(HTTPRequest.class);
|
||||
String payload = "I screamed------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHHH";
|
||||
IllegalStateException thrown =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> setPayloadMultipart(request, "lol", "cat", CSV_UTF_8, payload, random));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains(
|
||||
"Multipart data contains autogenerated boundary: "
|
||||
+ "------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
}
|
||||
}
|
69
util/src/test/java/google/registry/util/YamlUtilsTest.java
Normal file
69
util/src/test/java/google/registry/util/YamlUtilsTest.java
Normal file
|
@ -0,0 +1,69 @@
|
|||
// 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.util;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.util.YamlUtils.mergeYaml;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link YamlUtils}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class YamlUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testSuccess_mergeSimpleMaps() {
|
||||
String defaultYaml = join("one: ay", "two: bee", "three: sea");
|
||||
String customYaml = join("two: dee", "four: ignored");
|
||||
assertThat(mergeYaml(defaultYaml, customYaml)).isEqualTo("{one: ay, two: dee, three: sea}\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_mergeNestedMaps() {
|
||||
String defaultYaml = join("non: ay", "nested:", " blah: tim", " neat: 12");
|
||||
String customYaml = join("nested:", " blah: max", " extra: none");
|
||||
assertThat(mergeYaml(defaultYaml, customYaml))
|
||||
.isEqualTo(join("non: ay", "nested: {blah: max, neat: 12}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_listsAreOverridden() {
|
||||
String defaultYaml = join("non: ay", "list:", " - foo", " - bar", " - baz");
|
||||
String customYaml = join("list:", " - crackle", " - pop var");
|
||||
assertThat(mergeYaml(defaultYaml, customYaml))
|
||||
.isEqualTo(join("non: ay", "list: [crackle, pop var]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_mergeEmptyMap_isNoop() {
|
||||
String defaultYaml = join("one: ay", "two: bee", "three: sea");
|
||||
assertThat(mergeYaml(defaultYaml, "# Just a comment\n"))
|
||||
.isEqualTo("{one: ay, two: bee, three: sea}\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_mergeNamedMap_overwritesEntirelyWithNewKey() {
|
||||
String defaultYaml = join("one: ay", "two: bee", "threeMap:", " foo: bar", " baz: gak");
|
||||
assertThat(mergeYaml(defaultYaml, "threeMap: {time: money}"))
|
||||
.isEqualTo(join("one: ay", "two: bee", "threeMap: {time: money}"));
|
||||
}
|
||||
|
||||
private static String join(CharSequence... strings) {
|
||||
return Joiner.on('\n').join(strings) + "\n";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue