Remove Ofy (#1863)

So long, farewell, adios, ciao, sayonara, 再见!

TESTED=deployed to alpha and used `nomulus list_tlds` to confirm that the web app can receive and serve requests.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1863)
<!-- Reviewable:end -->
This commit is contained in:
Lai Jiang 2022-12-02 22:28:33 -05:00 committed by GitHub
parent 601aed388c
commit 1d7dfe4e07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 31 additions and 2381 deletions

View file

@ -6,5 +6,4 @@ node_modules/
repos/**
**/.idea/
*.jar
!third_party/**/*.jar
!/gradle/wrapper/**/*.jar

1
.gitignore vendored
View file

@ -14,7 +14,6 @@ gjf.out
*.jar
*.war
*.ear
!/third_party/**/*.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

View file

@ -355,8 +355,6 @@ subprojects {
}
}
if (project.name == 'third_party') return
project.tasks.test.dependsOn runPresubmits
def commonlyExcludedResources = ['**/*.java', '**/BUILD']

View file

@ -43,12 +43,6 @@ by Joshua Bloch in his book Effective Java -->
<property name="message" value='Your Javadocs appear to use invalid &lt;a&gt; tag syntax in @see tags. Please use the correct syntax: @see &lt;a href="http(s)://your_url"&gt;url_description&lt;/a&gt;'/>
</module>
<!-- Checks that our Ofy wrapper is used instead of the "real" ofy(). -->
<module name="RegexpSingleline">
<property name="format" value="com\.googlecode\.objectify\.ObjectifyService\.ofy"/>
<property name="message" value="Use google.registry.model.ofy.ObjectifyService.ofy(). Do not use com.googlecode.objectify.v4.ObjectifyService.ofy()."/>
</module>
<!-- Checks that java.util.Optional is used instead of Guava's Optional. -->
<module name="RegexpSingleline">
<property name="format" value="com\.google\.common\.base\.Optional"/>
@ -58,7 +52,7 @@ by Joshua Bloch in his book Effective Java -->
<!-- Checks that the deprecated ExpectedException is not used. -->
<module name="RegexpSingleline">
<property name="format" value="org\.junit\.rules\.ExpectedException"/>
<property name="message" value="Use assertThrows and expectThrows from JUnitBackports instead of the deprecated methods on ExpectedException."/>
<property name="message" value="Use assertThrows and expectThrows instead of the deprecated methods on ExpectedException."/>
</module>
<module name="LineLength">

View file

@ -163,11 +163,6 @@ configurations {
dependencies {
def deps = rootProject.dependencyMap
// Custom-built objectify jar at commit ecd5165, included in Nomulus
// release.
implementation files(
"${rootDir}/third_party/objectify/v4_1/objectify-4.1.3.jar")
testRuntimeOnly files(sourceSets.test.resources.srcDirs)
implementation deps['com.beust:jcommander']

View file

@ -21,7 +21,6 @@ import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryEnvironment;
import google.registry.config.SystemPropertySetter;
import google.registry.model.AppEngineEnvironment;
import google.registry.model.IdService;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.persistence.transaction.TransactionManagerFactory;
@ -63,10 +62,6 @@ public class RegistryPipelineWorkerInitializer implements JvmInitializer {
transactionManagerLazy = registryPipelineComponent.getJpaTransactionManager();
}
TransactionManagerFactory.setJpaTmOnBeamWorker(transactionManagerLazy::get);
// Masquerade all threads as App Engine threads, so we can create Ofy keys in the pipeline. Also
// loads all ofy entities.
new AppEngineEnvironment("s~" + registryPipelineComponent.getProjectId())
.setEnvironmentForAllThreads();
SystemPropertySetter.PRODUCTION_IMPL.setProperty(PROPERTY, "true");
// Use self-allocated IDs if requested. Note that this inevitably results in duplicate IDs from
// multiple workers, which can also collide with existing IDs in the database. So they cannot be

View file

@ -341,24 +341,4 @@ have been in the database for a certain period of time. -->
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View file

@ -139,24 +139,4 @@
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View file

@ -105,24 +105,4 @@
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View file

@ -135,24 +135,4 @@
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- See: https://code.google.com/p/objectify-appengine/wiki/Setup -->
<filter>
<filter-name>ObjectifyFilter</filter-name>
<filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ObjectifyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Register types with Objectify. -->
<filter>
<filter-name>OfyFilter</filter-name>
<filter-class>google.registry.model.ofy.OfyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>OfyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

View file

@ -1,124 +0,0 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.common.collect.ImmutableMap;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.ofy.ObjectifyService;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Sets up a fake {@link Environment} so that the following operations can be performed without the
* Datastore service:
*
* <ul>
* <li>Create Objectify {@code Keys}.
* <li>Instantiate Objectify objects.
* <li>Convert Datastore {@code Entities} to their corresponding Objectify objects.
* </ul>
*
* <p>User has the option to specify their desired {@code appId} string, which forms part of an
* Objectify {@code Key} and is included in the equality check. This feature makes it easy to
* compare a migrated object in SQL with the original in Objectify.
*
* <p>Note that conversion from Objectify objects to Datastore {@code Entities} still requires the
* Datastore service.
*/
@DeleteAfterMigration
public class AppEngineEnvironment {
private Environment environment;
/**
* Constructor for use by tests.
*
* <p>All test suites must use the same appId for environments, since when tearing down we do not
* clear cached environments in spawned threads. See {@link #unsetEnvironmentForAllThreads} for
* more information.
*/
public AppEngineEnvironment() {
/**
* Use AppEngineExtension's appId here so that ofy and sql entities can be compared with {@code
* Objects#equals()}. The choice of this value does not impact functional correctness.
*/
this("test");
}
/** Constructor for use by applications, e.g., BEAM pipelines. */
public AppEngineEnvironment(String appId) {
environment = createAppEngineEnvironment(appId);
}
public void setEnvironmentForCurrentThread() {
ApiProxy.setEnvironmentForCurrentThread(environment);
ObjectifyService.initOfy();
}
public void setEnvironmentForAllThreads() {
setEnvironmentForCurrentThread();
ApiProxy.setEnvironmentFactory(() -> environment);
}
public void unsetEnvironmentForCurrentThread() {
ApiProxy.clearEnvironmentForCurrentThread();
}
/**
* Unsets the test environment in all threads with best effort.
*
* <p>This method unsets the environment factory and clears the cached environment in the current
* thread (the main test runner thread). We do not clear the cache in spawned threads, even though
* they may be reused. This is not a problem as long as the appId stays the same: those threads
* are used only in AppEngine or BEAM tests, and expect the presence of an environment.
*/
public void unsetEnvironmentForAllThreads() {
unsetEnvironmentForCurrentThread();
try {
Method method = ApiProxy.class.getDeclaredMethod("clearEnvironmentFactory");
method.setAccessible(true);
method.invoke(null);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/** Returns a placeholder {@link Environment} that can return hardcoded AppId and Attributes. */
private static Environment createAppEngineEnvironment(String appId) {
return (Environment)
Proxy.newProxyInstance(
Environment.class.getClassLoader(),
new Class[] {Environment.class},
(Object proxy, Method method, Object[] args) -> {
switch (method.getName()) {
case "getAppId":
return appId;
case "getAttributes":
return ImmutableMap.<String, Object>of();
default:
throw new UnsupportedOperationException(method.getName());
}
});
}
/** Returns true if the current thread is in an App Engine Environment. */
public static boolean isInAppEngineEnvironment() {
return ApiProxy.getCurrentEnvironment() != null;
}
}

View file

@ -18,13 +18,9 @@ import static com.google.common.base.Suppliers.memoizeWithExpiration;
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Supplier;
import google.registry.model.annotations.DeleteAfterMigration;
import java.time.Duration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/** Utility methods related to caching Datastore entities. */
public class CacheUtils {
@ -77,29 +73,4 @@ public class CacheUtils {
}
return caffeine;
}
/**
* A {@link CacheLoader} that automatically masquerade the background thread where the refresh
* action runs in to be an GAE thread.
*/
@DeleteAfterMigration
public abstract static class AppEngineEnvironmentCacheLoader<K, V> implements CacheLoader<K, V> {
private static final AppEngineEnvironment environment = new AppEngineEnvironment();
@Override
public @Nullable V reload(@NonNull K key, @NonNull V oldValue) throws Exception {
V value;
boolean isMasqueraded = false;
if (!AppEngineEnvironment.isInAppEngineEnvironment()) {
environment.setEnvironmentForCurrentThread();
isMasqueraded = true;
}
value = load(key);
if (isMasqueraded) {
environment.unsetEnvironmentForCurrentThread();
}
return value;
}
}
}

View file

@ -32,7 +32,6 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.annotations.IdAllocation;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.transfer.TransferData;
@ -354,7 +353,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
}
static final CacheLoader<VKey<? extends EppResource>, EppResource> CACHE_LOADER =
new AppEngineEnvironmentCacheLoader<VKey<? extends EppResource>, EppResource>() {
new CacheLoader<VKey<? extends EppResource>, EppResource>() {
@Override
public EppResource load(VKey<? extends EppResource> key) {

View file

@ -22,7 +22,6 @@ import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.common.flogger.FluentLogger;
import google.registry.beam.common.RegistryPipelineWorkerInitializer;
import google.registry.config.RegistryEnvironment;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.DatabaseMigrationStateSchedule;
import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState;
import java.math.BigInteger;
@ -33,7 +32,6 @@ import org.joda.time.DateTime;
/**
* Allocates a {@link long} to use as a {@code @Id}, (part) of the primary SQL key for an entity.
*/
@DeleteAfterMigration
public final class IdService {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();

View file

@ -1,47 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.annotations;
import com.googlecode.objectify.annotation.Entity;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for an Objectify {@link Entity} to indicate that it should not be backed up by the
* default Datastore backup configuration (it may be backed up by something else).
*/
@DeleteAfterMigration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface NotBackedUp {
Reason reason();
/** Reasons why a given entity does not need to be be backed up. */
enum Reason {
/** This entity is transient by design and has only a short-term useful lifetime. */
TRANSIENT,
/** This entity's data is already regularly pulled down from an external source. */
EXTERNALLY_SOURCED,
/** This entity is generated automatically by the app and will be recreated if need be. */
AUTO_GENERATED,
/** Commit log entities are exported separately from the regular backups, by design. */
COMMIT_LOGS
}
}

View file

@ -1,32 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.annotations;
import com.googlecode.objectify.annotation.Entity;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for an Objectify {@link Entity} to indicate that it is a "virtual entity".
*
* <p>A virtual entity type exists only to define part of the parentage key hierarchy for its child
* entities, and is never actually persisted and thus has no fields besides its ID field.
*/
@DeleteAfterMigration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface VirtualEntity {}

View file

@ -1,73 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.common;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.IdService.allocateId;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import com.google.appengine.api.users.User;
import com.google.common.base.Splitter;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import java.util.List;
/**
* A helper class to convert email addresses to GAE user ids. It does so by persisting a User
* object with the email address to Datastore, and then immediately reading it back.
*/
@Entity
@NotBackedUp(reason = Reason.TRANSIENT)
public class GaeUserIdConverter extends ImmutableObject {
@Id
public long id;
User user;
/**
* Converts an email address to a GAE user id.
*
* @return Numeric GAE user id (in String form), or null if email address has no GAE id
*/
public static String convertEmailAddressToGaeUserId(String emailAddress) {
final GaeUserIdConverter gaeUserIdConverter = new GaeUserIdConverter();
gaeUserIdConverter.id = allocateId();
List<String> emailParts = Splitter.on('@').splitToList(emailAddress);
checkState(emailParts.size() == 2, "'%s' is not a valid email address", emailAddress);
gaeUserIdConverter.user = new User(emailAddress, emailParts.get(1));
try {
// Perform these operations in a transactionless context to avoid enlisting in some outer
// transaction (if any).
auditedOfy()
.doTransactionless(
() -> {
auditedOfy().saveWithoutBackup().entity(gaeUserIdConverter).now();
return null;
});
// The read must be done in its own transaction to avoid reading from the session cache.
return auditedOfy()
.transactNew(() -> auditedOfy().load().entity(gaeUserIdConverter).now().user.getUserId());
} finally {
auditedOfy()
.doTransactionless(
() -> auditedOfy().deleteWithoutBackup().entity(gaeUserIdConverter).now());
}
}
}

View file

@ -1,90 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.cmd.DeleteType;
import com.googlecode.objectify.cmd.Deleter;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* A Deleter that forwards to {@code auditedOfy().delete()}, but can be augmented via subclassing to
* do custom processing on the keys to be deleted prior to their deletion.
*/
@DeleteAfterMigration
abstract class AugmentedDeleter implements Deleter {
private final Deleter delegate = ofy().delete();
/** Extension method to allow this Deleter to do extra work prior to the actual delete. */
protected abstract void handleDeletion(Iterable<Key<?>> keys);
private void handleDeletionStream(Stream<?> entityStream) {
handleDeletion(entityStream.map(Key::create).collect(toImmutableList()));
}
@Override
public Result<Void> entities(Iterable<?> entities) {
handleDeletionStream(Streams.stream(entities));
return delegate.entities(entities);
}
@Override
public Result<Void> entities(Object... entities) {
handleDeletionStream(Arrays.stream(entities));
return delegate.entities(entities);
}
@Override
public Result<Void> entity(Object entity) {
handleDeletionStream(Stream.of(entity));
return delegate.entity(entity);
}
@Override
public Result<Void> key(Key<?> key) {
handleDeletion(ImmutableList.of(key));
return delegate.keys(key);
}
@Override
public Result<Void> keys(Iterable<? extends Key<?>> keys) {
// Magic to convert the type Iterable<? extends Key<?>> (a family of types which allows for
// homogeneous iterables of a fixed Key<T> type, e.g. List<Key<Lock>>, and is convenient for
// callers) into the type Iterable<Key<?>> (a concrete type of heterogeneous keys, which is
// convenient for users).
handleDeletion(ImmutableList.copyOf(keys));
return delegate.keys(keys);
}
@Override
public Result<Void> keys(Key<?>... keys) {
handleDeletion(Arrays.asList(keys));
return delegate.keys(keys);
}
/** Augmenting this gets ugly; you can always just use keys(Key.create(...)) instead. */
@Override
public DeleteType type(Class<?> clazz) {
throw new UnsupportedOperationException();
}
}

View file

@ -1,63 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.google.appengine.api.datastore.Entity;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.cmd.Saver;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Arrays;
import java.util.Map;
/**
* A Saver that forwards to {@code ofy().save()}, but can be augmented via subclassing to do custom
* processing on the entities to be saved prior to their saving.
*/
@DeleteAfterMigration
abstract class AugmentedSaver implements Saver {
private final Saver delegate = ofy().save();
/** Extension method to allow this Saver to do extra work prior to the actual save. */
protected abstract void handleSave(Iterable<?> entities);
@Override
public <E> Result<Map<Key<E>, E>> entities(Iterable<E> entities) {
handleSave(entities);
return delegate.entities(entities);
}
@Override
@SafeVarargs
public final <E> Result<Map<Key<E>, E>> entities(E... entities) {
handleSave(Arrays.asList(entities));
return delegate.entities(entities);
}
@Override
public <E> Result<Key<E>> entity(E entity) {
handleSave(ImmutableList.of(entity));
return delegate.entity(entity);
}
@Override
public Entity toEntity(Object pojo) {
// No call to the extension method, since toEntity() doesn't do any actual saving.
return delegate.toEntity(pojo);
}
}

View file

@ -1,84 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableSet;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.util.Clock;
import java.util.function.Supplier;
/** Wrapper for {@link Supplier} that associates a time with each attempt. */
@DeleteAfterMigration
public class CommitLoggedWork<R> implements Runnable {
private final Supplier<R> work;
private final Clock clock;
/**
* Temporary place to store the result of a non-void work.
*
* <p>We don't want to return the result directly because we are going to try to recover from a
* {@link com.google.appengine.api.datastore.DatastoreTimeoutException} deep inside Objectify when
* it tries to commit the transaction. When an exception is thrown the return value would be lost,
* but sometimes we will be able to determine that we actually succeeded despite the timeout, and
* we'll want to get the result.
*/
private R result;
/**
* Temporary place to store the mutations belonging to the commit log manifest.
*
* <p>These are used along with the manifest to determine whether a transaction succeeded.
*/
protected ImmutableSet<ImmutableObject> mutations = ImmutableSet.of();
/** Lifecycle marker to track whether {@link #run} has been called. */
private boolean runCalled;
CommitLoggedWork(Supplier<R> work, Clock clock) {
this.work = work;
this.clock = clock;
}
protected TransactionInfo createNewTransactionInfo() {
return new TransactionInfo(clock.nowUtc());
}
boolean hasRun() {
return runCalled;
}
R getResult() {
checkState(runCalled, "Cannot call getResult() before run()");
return result;
}
@Override
public void run() {
// The previous time will generally be null, except when using transactNew.
TransactionInfo previous = Ofy.TRANSACTION_INFO.get();
// Set the time to be used for "now" within the transaction.
try {
Ofy.TRANSACTION_INFO.set(createNewTransactionInfo());
result = work.get();
} finally {
Ofy.TRANSACTION_INFO.set(previous);
}
runCalled = true;
}
}

View file

@ -1,140 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.googlecode.objectify.ObjectifyService.factory;
import static google.registry.util.TypeUtils.hasAnnotation;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceConfig;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.config.RegistryEnvironment;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.GaeUserIdConverter;
/**
* An instance of Ofy, obtained via {@code #auditedOfy()}, should be used to access all persistable
* objects. The class contains a static initializer to call factory().register(...) on all
* persistable objects in this package.
*/
@DeleteAfterMigration
public class ObjectifyService {
/** A singleton instance of our Ofy wrapper. */
private static final Ofy OFY = new Ofy(null);
/**
* Returns the singleton {@link Ofy} instance, signifying that the caller has been audited for the
* Registry 3.0 conversion.
*/
public static Ofy auditedOfy() {
return OFY;
}
static {
initOfyOnce();
}
/** Ensures that Objectify has been fully initialized. */
public static void initOfy() {
// This method doesn't actually do anything; it's here so that callers have something to call
// to ensure that the static initialization of ObjectifyService has been performed (which Java
// guarantees will happen exactly once, before any static methods are invoked).
//
// See JLS section 12.4: http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4
}
/**
* Performs static initialization for Objectify to register types and do other setup.
*
* <p>This method is non-idempotent, so it should only be called exactly once, which is achieved
* by calling it from this class's static initializer block.
*/
private static void initOfyOnce() {
// Set an ObjectifyFactory that uses our extended ObjectifyImpl.
// The "false" argument means that we are not using the v5-style Objectify embedded entities.
com.googlecode.objectify.ObjectifyService.setFactory(
new ObjectifyFactory(false) {
@Override
protected AsyncDatastoreService createRawAsyncDatastoreService(
DatastoreServiceConfig cfg) {
// In the unit test environment, wrap the Datastore service in a proxy that can be used
// to examine the number of requests sent to Datastore.
AsyncDatastoreService service = super.createRawAsyncDatastoreService(cfg);
return RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
? new RequestCapturingAsyncDatastoreService(service)
: service;
}
});
registerEntityClasses(ImmutableSet.of(GaeUserIdConverter.class));
}
/** Register classes that can be persisted via Objectify as Datastore entities. */
private static void registerEntityClasses(
ImmutableSet<Class<? extends ImmutableObject>> entityClasses) {
// Register all the @Entity classes before any @EntitySubclass classes so that we can check
// that every @Entity registration is a new kind and every @EntitySubclass registration is not.
// This is future-proofing for Objectify 5.x where the registration logic gets less lenient.
for (Class<?> clazz :
Streams.concat(
entityClasses.stream().filter(hasAnnotation(Entity.class)),
entityClasses.stream().filter(hasAnnotation(Entity.class).negate()))
.collect(toImmutableSet())) {
String kind = Key.getKind(clazz);
boolean registered = factory().getMetadata(kind) != null;
if (clazz.isAnnotationPresent(Entity.class)) {
// Objectify silently replaces current registration for a given kind string when a different
// class is registered again for this kind. For simplicity's sake, throw an exception on any
// re-registration.
checkState(
!registered,
"Kind '%s' already registered, cannot register new @Entity %s",
kind,
clazz.getCanonicalName());
} else if (clazz.isAnnotationPresent(EntitySubclass.class)) {
// Ensure that any @EntitySubclass classes have also had their parent @Entity registered,
// which Objectify nominally requires but doesn't enforce in 4.x (though it may in 5.x).
checkState(
registered,
"No base entity for kind '%s' registered yet, cannot register new @EntitySubclass %s",
kind,
clazz.getCanonicalName());
}
com.googlecode.objectify.ObjectifyService.register(clazz);
// Autogenerated ids make the commit log code very difficult since we won't always be able
// to create a key for an entity immediately when requesting a save. So, we require such
// entities to implement google.registry.model.Buildable as its build() function allocates the
// id to the entity.
if (factory().getMetadata(clazz).getKeyMetadata().isIdGeneratable()) {
checkState(
Buildable.class.isAssignableFrom(clazz),
"Can't register %s: Entity with autogenerated ids (@Id on a Long) must implement"
+ " google.registry.model.Buildable.",
kind);
}
}
}
}

View file

@ -1,374 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.uniqueIndex;
import static com.googlecode.objectify.ObjectifyService.ofy;
import static google.registry.config.RegistryConfig.getBaseOfyRetryDuration;
import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.cmd.Deleter;
import com.googlecode.objectify.cmd.Loader;
import com.googlecode.objectify.cmd.Saver;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.VirtualEntity;
import google.registry.model.ofy.ReadOnlyWork.KillTransactionException;
import google.registry.util.Clock;
import google.registry.util.NonFinalForTesting;
import google.registry.util.Sleeper;
import google.registry.util.SystemClock;
import google.registry.util.SystemSleeper;
import java.lang.annotation.Annotation;
import java.util.Objects;
import java.util.function.Supplier;
import javax.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* A wrapper around ofy().
*
* <p>The primary purpose of this class is to add functionality to support commit logs. It is
* simpler to wrap {@link Objectify} rather than extend it because this way we can remove some
* methods that we don't really want exposed and add some shortcuts.
*/
@DeleteAfterMigration
public class Ofy {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Default clock for transactions that don't provide one. */
@NonFinalForTesting
static Clock clock = new SystemClock();
/** Default sleeper for transactions that don't provide one. */
@NonFinalForTesting
static Sleeper sleeper = new SystemSleeper();
/**
* An injected clock that overrides the static clock.
*
* <p>Eventually the static clock should go away when we are 100% injected, but for now we need to
* preserve the old way of overriding the clock in tests by changing the static field.
*/
private final Clock injectedClock;
/** Retry for 8^2 * 100ms = ~25 seconds. */
private static final int NUM_RETRIES = 8;
@Inject
public Ofy(Clock injectedClock) {
this.injectedClock = injectedClock;
}
/**
* Thread local transaction info. There can only be one active transaction on a thread at a given
* time, and this will hold metadata for it.
*/
static final ThreadLocal<TransactionInfo> TRANSACTION_INFO = new ThreadLocal<>();
/** Returns the wrapped Objectify's ObjectifyFactory. */
public ObjectifyFactory factory() {
return ofy().factory();
}
/** Clears the session cache. */
public void clearSessionCache() {
ofy().clear();
}
boolean inTransaction() {
return ofy().getTransaction() != null;
}
public void assertInTransaction() {
checkState(inTransaction(), "Must be called in a transaction");
}
/** Load from Datastore. */
public Loader load() {
return ofy().load();
}
/**
* Delete, augmented to enroll the deleted entities in a commit log.
*
* <p>We only allow this in transactions so commit logs can be written in tandem with the delete.
*/
public Deleter delete() {
return deleteIgnoringReadOnlyWithBackup();
}
/**
* Delete, without any augmentations except to check that we're not saving any virtual entities.
*
* <p>No backups get written.
*/
public Deleter deleteWithoutBackup() {
return deleteIgnoringReadOnlyWithoutBackup();
}
/**
* Save, augmented to enroll the saved entities in a commit log and to check that we're not saving
* virtual entities.
*
* <p>We only allow this in transactions so commit logs can be written in tandem with the save.
*/
public Saver save() {
return saveIgnoringReadOnlyWithBackup();
}
/**
* Save, without any augmentations except to check that we're not saving any virtual entities.
*
* <p>No backups get written.
*/
public Saver saveWithoutBackup() {
return saveIgnoringReadOnlyWithoutBackup();
}
/** Save, ignoring any backups or any read-only settings. */
public Saver saveIgnoringReadOnlyWithoutBackup() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable<?> entities) {
checkProhibitedAnnotations(entities, VirtualEntity.class);
}
};
}
/** Delete, ignoring any backups or any read-only settings. */
public Deleter deleteIgnoringReadOnlyWithoutBackup() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
checkProhibitedAnnotations(keys, VirtualEntity.class);
}
};
}
/** Save, ignoring any read-only settings (but still write commit logs). */
public Saver saveIgnoringReadOnlyWithBackup() {
return new AugmentedSaver() {
@Override
protected void handleSave(Iterable<?> entities) {
assertInTransaction();
checkState(
Streams.stream(entities).allMatch(Objects::nonNull), "Can't save a null entity.");
checkProhibitedAnnotations(entities, NotBackedUp.class, VirtualEntity.class);
ImmutableMap<Key<?>, ?> keysToEntities = uniqueIndex(entities, Key::create);
TRANSACTION_INFO.get().putSaves(keysToEntities);
}
};
}
/** Delete, ignoring any read-only settings (but still write commit logs). */
public Deleter deleteIgnoringReadOnlyWithBackup() {
return new AugmentedDeleter() {
@Override
protected void handleDeletion(Iterable<Key<?>> keys) {
assertInTransaction();
checkState(Streams.stream(keys).allMatch(Objects::nonNull), "Can't delete a null key.");
checkProhibitedAnnotations(keys, NotBackedUp.class, VirtualEntity.class);
TRANSACTION_INFO.get().putDeletes(keys);
}
};
}
private Clock getClock() {
return injectedClock == null ? clock : injectedClock;
}
/** Execute a transaction. */
<R> R transact(Supplier<R> work) {
// If we are already in a transaction, don't wrap in a CommitLoggedWork.
return inTransaction() ? work.get() : transactNew(work);
}
/**
* Execute a transaction.
*
* <p>This overload is used for transactions that don't return a value, formerly implemented using
* VoidWork.
*/
void transact(Runnable work) {
transact(
() -> {
work.run();
return null;
});
}
/** Pause the current transaction (if any) and complete this one before returning to it. */
public <R> R transactNew(Supplier<R> work) {
// Wrap the Work in a CommitLoggedWork so that we can give transactions a frozen view of time.
return transactCommitLoggedWork(new CommitLoggedWork<>(work, getClock()));
}
/**
* Pause the current transaction (if any) and complete this one before returning to it.
*
* <p>This overload is used for transactions that don't return a value, formerly implemented using
* VoidWork.
*/
void transactNew(Runnable work) {
transactNew(
() -> {
work.run();
return null;
});
}
/**
* Transact with commit logs and retry with exponential backoff.
*
* <p>This method is broken out from {@link #transactNew(Supplier)} for testing purposes.
*/
@VisibleForTesting
<R> R transactCommitLoggedWork(CommitLoggedWork<R> work) {
long baseRetryMillis = getBaseOfyRetryDuration().getMillis();
for (long attempt = 0, sleepMillis = baseRetryMillis;
true;
attempt++, sleepMillis *= 2) {
try {
ofy().transactNew(() -> {
work.run();
return null;
});
return work.getResult();
} catch (TransientFailureException
| DatastoreTimeoutException
| DatastoreFailureException e) {
// TransientFailureExceptions come from task queues and always mean nothing committed.
// TimestampInversionExceptions are thrown by our code and are always retryable as well.
// However, Datastore exceptions might get thrown even if the transaction succeeded.
if ((e instanceof DatastoreTimeoutException || e instanceof DatastoreFailureException)
&& work.hasRun()) {
return work.getResult();
}
if (attempt == NUM_RETRIES) {
throw e; // Give up.
}
sleeper.sleepUninterruptibly(Duration.millis(sleepMillis));
logger.atInfo().withCause(e).log(
"Retrying %s, attempt %d.", e.getClass().getSimpleName(), attempt);
}
}
}
/** A read-only transaction is useful to get strongly consistent reads at a shared timestamp. */
<R> R transactNewReadOnly(Supplier<R> work) {
ReadOnlyWork<R> readOnlyWork = new ReadOnlyWork<>(work, getClock());
try {
ofy().transactNew(() -> {
readOnlyWork.run();
return null;
});
} catch (TransientFailureException | DatastoreTimeoutException | DatastoreFailureException e) {
// These are always retryable for a read-only operation.
return transactNewReadOnly(work);
} catch (KillTransactionException e) {
// Expected; we killed the transaction as a safety measure, and now we can return the result.
return readOnlyWork.getResult();
}
throw new AssertionError(); // How on earth did we get here?
}
void transactNewReadOnly(Runnable work) {
transactNewReadOnly(
() -> {
work.run();
return null;
});
}
/** Execute some work in a transactionless context. */
public <R> R doTransactionless(Supplier<R> work) {
try {
com.googlecode.objectify.ObjectifyService.push(
com.googlecode.objectify.ObjectifyService.ofy().transactionless());
return work.get();
} finally {
com.googlecode.objectify.ObjectifyService.pop();
}
}
/**
* Execute some work with a fresh session cache.
*
* <p>This is useful in cases where we want to load the latest possible data from Datastore but
* don't need point-in-time consistency across loads and consequently don't need a transaction.
* Note that unlike a transaction's fresh session cache, the contents of this cache will be
* discarded once the work completes, rather than being propagated into the enclosing session.
*/
public <R> R doWithFreshSessionCache(Supplier<R> work) {
try {
com.googlecode.objectify.ObjectifyService.push(
com.googlecode.objectify.ObjectifyService.factory().begin());
return work.get();
} finally {
com.googlecode.objectify.ObjectifyService.pop();
}
}
/** Get the time associated with the start of this particular transaction attempt. */
DateTime getTransactionTime() {
assertInTransaction();
return TRANSACTION_INFO.get().transactionTime;
}
/**
* Returns the @Entity-annotated base class for an object that is either an {@code Key<?>} or an
* object of an entity class registered with Objectify.
*/
@VisibleForTesting
static Class<?> getBaseEntityClassFromEntityOrKey(Object entityOrKey) {
// Convert both keys and entities into keys, so that we get consistent behavior in either case.
Key<?> key = (entityOrKey instanceof Key<?> ? (Key<?>) entityOrKey : Key.create(entityOrKey));
// Get the entity class associated with this key's kind, which should be the base @Entity class
// from which the kind name is derived. Don't be tempted to use getMetadata(String kind) or
// getMetadataForEntity(T pojo) instead; the former won't throw an exception for an unknown
// kind (it just returns null) and the latter will return the @EntitySubclass if there is one.
return ofy().factory().getMetadata(key).getEntityClass();
}
/**
* Checks that the base @Entity classes for the provided entities or keys don't have any of the
* specified forbidden annotations.
*/
@SafeVarargs
private static void checkProhibitedAnnotations(
Iterable<?> entitiesOrKeys, Class<? extends Annotation>... annotations) {
for (Object entityOrKey : entitiesOrKeys) {
Class<?> entityClass = getBaseEntityClassFromEntityOrKey(entityOrKey);
for (Class<? extends Annotation> annotation : annotations) {
checkArgument(!entityClass.isAnnotationPresent(annotation),
"Can't save/delete a @%s entity: %s", annotation.getSimpleName(), entityClass);
}
}
}
}

View file

@ -1,44 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import google.registry.model.annotations.DeleteAfterMigration;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/** A filter that statically registers types with Objectify. */
@DeleteAfterMigration
public class OfyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
filterChain.doFilter(request, response);
}
@Override
public void init(FilterConfig config) {
// Make sure that we've registered all types before we do anything else with Objectify.
ObjectifyService.initOfy();
}
@Override
public void destroy() {}
}

View file

@ -1,42 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.util.Clock;
import java.util.function.Supplier;
/** Wrapper for {@link Supplier} that disallows mutations and fails the transaction at the end. */
@DeleteAfterMigration
class ReadOnlyWork<R> extends CommitLoggedWork<R> {
ReadOnlyWork(Supplier<R> work, Clock clock) {
super(work, clock);
}
@Override
protected TransactionInfo createNewTransactionInfo() {
return super.createNewTransactionInfo().setReadOnly();
}
@Override
public void run() {
super.run();
throw new KillTransactionException();
}
/** Exception used to exit a transaction. */
static class KillTransactionException extends RuntimeException {}
}

View file

@ -1,194 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static java.util.Collections.synchronizedList;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreAttributes;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Index;
import com.google.appengine.api.datastore.Index.IndexState;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.datastore.TransactionOptions;
import com.google.common.collect.ImmutableList;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
/** A proxy for {@link AsyncDatastoreService} that exposes call counts. */
@DeleteAfterMigration
public class RequestCapturingAsyncDatastoreService implements AsyncDatastoreService {
private final AsyncDatastoreService delegate;
// Each outer lists represents Datastore operations, with inner lists representing the keys or
// entities involved in that operation. We use static lists because we care about overall calls to
// Datastore, not calls via a specific instance of the service.
private static List<List<Key>> reads = synchronizedList(new ArrayList<List<Key>>());
private static List<List<Key>> deletes = synchronizedList(new ArrayList<List<Key>>());
private static List<List<Entity>> puts = synchronizedList(new ArrayList<List<Entity>>());
RequestCapturingAsyncDatastoreService(AsyncDatastoreService delegate) {
this.delegate = delegate;
}
public static List<List<Key>> getReads() {
return reads;
}
public static List<List<Key>> getDeletes() {
return deletes;
}
public static List<List<Entity>> getPuts() {
return puts;
}
@Override
public Collection<Transaction> getActiveTransactions() {
return delegate.getActiveTransactions();
}
@Override
public Transaction getCurrentTransaction() {
return delegate.getCurrentTransaction();
}
@Override
public Transaction getCurrentTransaction(Transaction transaction) {
return delegate.getCurrentTransaction(transaction);
}
@Override
public PreparedQuery prepare(Query query) {
return delegate.prepare(query);
}
@Override
public PreparedQuery prepare(Transaction transaction, Query query) {
return delegate.prepare(transaction, query);
}
@Override
public Future<KeyRange> allocateIds(String kind, long num) {
return delegate.allocateIds(kind, num);
}
@Override
public Future<KeyRange> allocateIds(Key parent, String kind, long num) {
return delegate.allocateIds(parent, kind, num);
}
@Override
public Future<Transaction> beginTransaction() {
return delegate.beginTransaction();
}
@Override
public Future<Transaction> beginTransaction(TransactionOptions transaction) {
return delegate.beginTransaction(transaction);
}
@Override
public Future<Void> delete(Key... keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(keys);
}
@Override
public Future<Void> delete(Iterable<Key> keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(keys);
}
@Override
public Future<Void> delete(Transaction transaction, Key... keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(transaction, keys);
}
@Override
public Future<Void> delete(Transaction transaction, Iterable<Key> keys) {
deletes.add(ImmutableList.copyOf(keys));
return delegate.delete(transaction, keys);
}
@Override
public Future<Entity> get(Key key) {
reads.add(ImmutableList.of(key));
return delegate.get(key);
}
@Override
public Future<Map<Key, Entity>> get(Iterable<Key> keys) {
reads.add(ImmutableList.copyOf(keys));
return delegate.get(keys);
}
@Override
public Future<Entity> get(Transaction transaction, Key key) {
reads.add(ImmutableList.of(key));
return delegate.get(transaction, key);
}
@Override
public Future<Map<Key, Entity>> get(Transaction transaction, Iterable<Key> keys) {
reads.add(ImmutableList.copyOf(keys));
return delegate.get(transaction, keys);
}
@Override
public Future<DatastoreAttributes> getDatastoreAttributes() {
return delegate.getDatastoreAttributes();
}
@Override
public Future<Map<Index, IndexState>> getIndexes() {
return delegate.getIndexes();
}
@Override
public Future<Key> put(Entity entity) {
puts.add(ImmutableList.of(entity));
return delegate.put(entity);
}
@Override
public Future<List<Key>> put(Iterable<Entity> entities) {
puts.add(ImmutableList.copyOf(entities));
return delegate.put(entities);
}
@Override
public Future<Key> put(Transaction transaction, Entity entity) {
puts.add(ImmutableList.of(entity));
return delegate.put(transaction, entity);
}
@Override
public Future<List<Key>> put(Transaction transaction, Iterable<Entity> entities) {
puts.add(ImmutableList.copyOf(entities));
return delegate.put(transaction, entities);
}
}

View file

@ -1,73 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.toMap;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.Key;
import google.registry.model.annotations.DeleteAfterMigration;
import java.util.Map;
import org.joda.time.DateTime;
/** Metadata for an {@link Ofy} transaction that saves commit logs. */
@DeleteAfterMigration
public class TransactionInfo {
@VisibleForTesting
public enum Delete {
SENTINEL
}
/** Logical "now" of the transaction. */
DateTime transactionTime;
/** Whether this is a read-only transaction. */
private boolean readOnly;
/**
* Accumulator of save/delete operations performed in transaction.
*
* <p>The {@link ImmutableMap} builder provides us the benefit of not permitting duplicates.
* This allows us to avoid potential race conditions where the same key is mutated twice in a
* transaction.
*/
private final ImmutableMap.Builder<Key<?>, Object> changesBuilder = new ImmutableMap.Builder<>();
TransactionInfo(DateTime now) {
this.transactionTime = now;
}
TransactionInfo setReadOnly() {
this.readOnly = true;
return this;
}
void assertNotReadOnly() {
checkState(!readOnly, "This is a read only transaction.");
}
void putSaves(Map<Key<?>, ?> keysToEntities) {
assertNotReadOnly();
changesBuilder.putAll(keysToEntities);
}
void putDeletes(Iterable<Key<?>> keys) {
assertNotReadOnly();
changesBuilder.putAll(toMap(keys, k -> Delete.SENTINEL));
}
}

View file

@ -75,7 +75,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
* Constructs a {@link VKey} for an {@link EppResource } from the string representation.
*
* <p>The string representation is obtained from the {@link #stringify()} function and like this:
* {@code kind:TestObject@sql:rO0ABXQAA2Zvbw}
* {@code kind:SomeEntity@sql:rO0ABXQAA2Zvbw}
*/
public static <T extends EppResource> VKey<T> createEppVKeyFromString(String keyString) {
ImmutableMap<String, String> kvs =

View file

@ -149,14 +149,6 @@ public abstract class QueryComposer<T> {
/**
* Enum used to specify comparison operations, e.g. {@code where("fieldName", Comparator.NE,
* "someval")'}.
*
* <p>These contain values that specify the comparison behavior for both objectify and criteria
* queries. For objectify, we provide a string to be appended to the field name in a {@code
* filter()} expression. For criteria queries we provide a function that knows how to obtain a
* {@link WhereOperator} from a {@link CriteriaBuilder}.
*
* <p>Note that the objectify strings for comparators other than equality are preceded by a space
* because {@code filter()} expects the fieldname to be separated from the operator by a space.
*/
public enum Comparator {
/**

View file

@ -19,12 +19,12 @@ import static google.registry.config.RegistryConfig.ConfigModule.TmchCaMode.PROD
import static google.registry.config.RegistryConfig.getSingletonCacheRefreshDuration;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import google.registry.model.CacheUtils;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.tmch.TmchCrl;
import google.registry.util.Clock;
import google.registry.util.X509Utils;
@ -78,7 +78,7 @@ public final class TmchCertificateAuthority {
private static final LoadingCache<TmchCaMode, X509CRL> CRL_CACHE =
CacheUtils.newCacheBuilder(getSingletonCacheRefreshDuration())
.build(
new AppEngineEnvironmentCacheLoader<TmchCaMode, X509CRL>() {
new CacheLoader<TmchCaMode, X509CRL>() {
@Override
public X509CRL load(final TmchCaMode tmchCaMode) throws GeneralSecurityException {
Optional<TmchCrl> storedCrl = TmchCrl.get();

View file

@ -1,80 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools;
import com.google.appengine.api.datastore.Entity;
import com.google.auto.value.AutoValue;
import com.google.common.base.Objects;
/**
* Wraps {@link Entity} for ease of processing in collections.
*
* <p>Note that the {@link #hashCode}/{@link #equals} methods are based on both the entity's key and
* its properties.
*/
final class EntityWrapper {
private static final String TEST_ENTITY_KIND = "TestEntity";
private final Entity entity;
EntityWrapper(Entity entity) {
this.entity = entity;
}
public Entity getEntity() {
return entity;
}
@Override
public boolean equals(Object that) {
if (that instanceof EntityWrapper) {
EntityWrapper thatEntity = (EntityWrapper) that;
return entity.equals(thatEntity.entity)
&& entity.getProperties().equals(thatEntity.entity.getProperties());
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(entity.getKey(), entity.getProperties());
}
@Override
public String toString() {
return "EntityWrapper(" + entity + ")";
}
public static EntityWrapper from(int id, Property... properties) {
Entity entity = new Entity(TEST_ENTITY_KIND, id);
for (Property prop : properties) {
entity.setProperty(prop.name(), prop.value());
}
return new EntityWrapper(entity);
}
@AutoValue
abstract static class Property {
static Property create(String name, Object value) {
return new AutoValue_EntityWrapper_Property(name, value);
}
abstract String name();
abstract Object value();
}
}

View file

@ -29,7 +29,6 @@ import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import google.registry.config.RegistryConfig;
import google.registry.model.ofy.ObjectifyService;
import google.registry.persistence.transaction.TransactionManagerFactory;
import google.registry.tools.AuthModule.LoginRequiredException;
import google.registry.tools.params.ParameterFactory;
@ -256,15 +255,6 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
}
installer.install(options);
// Database setup -- we also only ever do this if "installer" is null, just so that it's
// only done once.
// Ensure that all entity classes are loaded before command code runs.
ObjectifyService.initOfy();
// Make sure we start the command with a clean cache, so that any previous command won't
// interfere with this one.
ObjectifyService.auditedOfy().clearSessionCache();
// Enable Cloud SQL for command that needs remote API as they will very likely use
// Cloud SQL after the database migration. Note that the DB password is stored in Datastore
// and it is already initialized above.

View file

@ -27,7 +27,6 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.domain.Domain;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.tools.CommandWithConnection;
@ -199,7 +198,6 @@ public class CreateSyntheticDomainHistoriesCommand extends ConfirmingCommand
} catch (IOException e) {
throw new RuntimeException(e);
}
ObjectifyService.initOfy();
return installer;
}

View file

@ -928,7 +928,6 @@ soy.$$cleanHtml = function(value, opt_safeTags) {
};
// LINT.IfChange(htmlToText)
/**
* Converts HTML to plain text by removing tags, normalizing spaces and
* converting entities.
@ -1008,10 +1007,6 @@ soy.$$htmlToText = function(value) {
/** @private @const */
soy.BLOCK_TAGS_RE_ =
/^\/?(address|blockquote|dd|div|dl|dt|h[1-6]|hr|li|ol|p|pre|table|tr|ul)$/i;
// LINT.ThenChange(
// ../../../third_party/java_src/soy/java/com/google/template/soy/basicfunctions/HtmlToText.java,
// ../../../third_party/java_src/soy/python/runtime/sanitize.py:htmlToText)
/**
* Escapes HTML, except preserves entities.

View file

@ -20,7 +20,6 @@
package google.registry.beam;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@ -28,7 +27,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -298,15 +296,6 @@ public class TestPipelineExtension extends Pipeline
enableAbandonedNodeEnforcement(true);
}
// Clear this property so that when default Guava ThreadFactory is created
// it will not think that it is in App Engine and return an unusable
// ThreadFactory.
System.clearProperty("com.google.appengine.runtime.environment");
assertWithMessage(
"Beam pipelines don't run in an App Engine environment, and thus"
+ " the tests shouldn't be mocking one either.")
.that(isAppEngine())
.isFalse();
}
@Override
@ -512,25 +501,6 @@ public class TestPipelineExtension extends Pipeline
}
}
// Adapted from Guava's MoreExecutors (where it is a private method)
private static boolean isAppEngine() {
if (System.getProperty("com.google.appengine.runtime.environment") == null) {
return false;
} else {
try {
return Class.forName("com.google.apphosting.api.ApiProxy")
.getMethod("getCurrentEnvironment")
.invoke(null)
!= null;
} catch (ClassNotFoundException
| InvocationTargetException
| IllegalAccessException
| NoSuchMethodException e) {
return false;
}
}
}
private static class IsEmptyVisitor extends PipelineVisitor.Defaults {
private boolean empty = true;

View file

@ -25,9 +25,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.persistence.VKey;
import google.registry.testing.AppEngineExtension;
import google.registry.util.CidrAddressBlock;
import java.lang.reflect.Field;
@ -38,6 +36,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -50,7 +50,6 @@ public class ImmutableObjectTest {
AppEngineExtension.builder()
.withCloudSql()
.withJpaUnitTestEntities(ValueObject.class)
.withOfyTestEntities(ValueObject.class)
.build();
/** Simple subclass of ImmutableObject. */
@ -266,21 +265,19 @@ public class ImmutableObjectTest {
/** Subclass of ImmutableObject with keys to other objects. */
public static class RootObject extends ImmutableObject {
Key<ValueObject> hydrateMe;
VKey<ValueObject> hydrateMe;
@DoNotHydrate
Key<ValueObject> skipMe;
@DoNotHydrate VKey<ValueObject> skipMe;
Map<String, Key<ValueObject>> map;
Map<String, VKey<ValueObject>> map;
Set<Key<ValueObject>> set;
Set<VKey<ValueObject>> set;
}
/** Simple subclass of ImmutableObject. */
@Entity
@javax.persistence.Entity
public static class ValueObject extends ImmutableObject {
@Id @javax.persistence.Id long id;
@Id long id;
String value;

View file

@ -1,52 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.common;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link GaeUserIdConverter}. */
public class GaeUserIdConverterTest {
@RegisterExtension
public final AppEngineExtension appEngine = AppEngineExtension.builder().withCloudSql().build();
@AfterEach
void verifyNoLingeringEntities() {
assertThat(auditedOfy().load().type(GaeUserIdConverter.class).count()).isEqualTo(0);
}
@Test
void testSuccess() {
assertThat(GaeUserIdConverter.convertEmailAddressToGaeUserId("example@example.com"))
.matches("[0-9]+");
}
@Test
void testSuccess_inTransaction() {
auditedOfy()
.transactNew(
() -> {
assertThat(GaeUserIdConverter.convertEmailAddressToGaeUserId("example@example.com"))
.matches("[0-9]+");
return null;
});
}
}

View file

@ -1,32 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for our replacement for ObjectifyService. */
public class ObjectifyServiceTest {
@RegisterExtension
public final AppEngineExtension appEngine = AppEngineExtension.builder().withCloudSql().build();
@Test
void test_initOfy_canBeCalledTwice() {
ObjectifyService.initOfy();
ObjectifyService.initOfy();
}
}

View file

@ -1,104 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.ofy;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ofy.ObjectifyService.initOfy;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.ObjectifyFilter;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for our replacement Objectify filter. */
class OfyFilterTest {
private LocalServiceTestHelper helper;
private ObjectifyFactory factory;
// We can't use AppEngineExtension, because it triggers the precise behavior that we are testing.
@BeforeEach
void beforeEach() {
helper = new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()).setUp();
// Clear out the factory so that it requires re-registration on each test method.
// Otherwise, static registration of types in one method would persist across methods.
initOfy();
factory = ObjectifyService.factory();
ObjectifyService.setFactory(new ObjectifyFactory(false));
}
@AfterEach
void afterEach() {
ObjectifyFilter.complete();
ObjectifyService.setFactory(factory);
ObjectifyFilter.complete();
helper.tearDown();
}
@RegisterExtension
final JpaIntegrationTestExtension database =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
/**
* Key.create looks up kind metadata for the class of the object it is given. If this happens
* before the first reference to ObjectifyService, which statically triggers type registrations,
* then the create will fail. Note that this is only a problem if the type in question doesn't
* call ObjectifyService.allocateId() inside its own builder or create method, since if it does
* that would trigger the statics as well. In this example, Registrar has a string id, so the bug
* occurs, were it not for OfyFilter.
*/
@Test
void testFilterRegistersTypes() {
UnregisteredEntity entity = new UnregisteredEntity(5L);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> Key.create(entity));
assertThat(e)
.hasMessageThat()
.isEqualTo(
"class google.registry.model.ofy.OfyFilterTest$UnregisteredEntity "
+ "has not been registered");
}
/** The filter should register all types for us. */
@Test
void testKeyCreateAfterFilter() {
new OfyFilter().init(null);
GaeUserIdConverter userIdConverter = new GaeUserIdConverter();
userIdConverter.id = 1;
Key.create(userIdConverter);
}
@Entity
private static class UnregisteredEntity {
@Id long id;
UnregisteredEntity(long id) {
this.id = id;
}
}
}

View file

@ -21,13 +21,10 @@ import static google.registry.util.BuildPathUtils.getResourcesDir;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.HostAndPort;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.ofy.OfyFilter;
import google.registry.module.backend.BackendServlet;
import google.registry.module.frontend.FrontendServlet;
import java.net.URL;
import java.nio.file.Path;
import javax.servlet.Filter;
/** Lightweight HTTP server for testing the Nomulus Admin and Registrar consoles. */
public final class RegistryTestServer {
@ -87,15 +84,11 @@ public final class RegistryTestServer {
route("/registry-lock-post", FrontendServlet.class),
route("/registry-lock-verify", FrontendServlet.class));
private static final ImmutableList<Class<? extends Filter>> FILTERS = ImmutableList.of(
ObjectifyFilter.class,
OfyFilter.class);
private final TestServer server;
/** @see TestServer#TestServer(HostAndPort, ImmutableMap, ImmutableList, ImmutableList) */
public RegistryTestServer(HostAndPort address) {
server = new TestServer(address, RUNFILES, ROUTES, FILTERS);
server = new TestServer(address, RUNFILES, ROUTES);
}
/** @see TestServer#start() */

View file

@ -18,20 +18,14 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.throwIfInstanceOf;
import static google.registry.util.TypeUtils.instantiate;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Uninterruptibles;
import java.io.IOException;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import javax.annotation.Nullable;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -39,49 +33,33 @@ import javax.servlet.http.HttpServletResponse;
/**
* Servlet that wraps a servlet and delegates request execution to a queue.
*
* <p>The actual invocation of the delegate does not happen within this servlet's lifecycle.
* Therefore, the task on the queue must manually invoke filters within the queue task.
*
* @see TestServer
*/
public final class ServletWrapperDelegatorServlet extends HttpServlet {
private final Queue<FutureTask<Void>> requestQueue;
private final Class<? extends HttpServlet> servletClass;
private final ImmutableList<Class<? extends Filter>> filterClasses;
ServletWrapperDelegatorServlet(
Class<? extends HttpServlet> servletClass,
ImmutableList<Class<? extends Filter>> filterClasses,
Queue<FutureTask<Void>> requestQueue) {
this.servletClass = servletClass;
this.filterClasses = filterClasses;
this.requestQueue = checkNotNull(requestQueue, "requestQueue");
}
@Override
public void service(final HttpServletRequest req, final HttpServletResponse rsp)
throws ServletException, IOException {
FutureTask<Void> task = new FutureTask<>(new Callable<Void>() {
@Nullable
@Override
public Void call() throws ServletException, IOException {
// Simulate the full filter chain with the servlet at the end.
final Iterator<Class<? extends Filter>> filtersIter = filterClasses.iterator();
FilterChain filterChain =
new FilterChain() {
FutureTask<Void> task =
new FutureTask<>(
new Callable<Void>() {
@Nullable
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (filtersIter.hasNext()) {
instantiate(filtersIter.next()).doFilter(request, response, this);
} else {
instantiate(servletClass).service(request, response);
}
}};
filterChain.doFilter(req, rsp);
return null;
}});
public Void call() throws ServletException, IOException {
instantiate(servletClass).service(req, rsp);
return null;
}
});
requestQueue.add(task);
try {
Uninterruptibles.getUninterruptibly(task);

View file

@ -34,7 +34,6 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import javax.servlet.Filter;
import javax.servlet.http.HttpServlet;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Server;
@ -85,13 +84,10 @@ public final class TestServer {
* @param routes list of servlet endpoints
*/
public TestServer(
HostAndPort address,
ImmutableMap<String, Path> runfiles,
ImmutableList<Route> routes,
ImmutableList<Class<? extends Filter>> filters) {
HostAndPort address, ImmutableMap<String, Path> runfiles, ImmutableList<Route> routes) {
urlAddress = createUrlAddress(address);
server.addConnector(createConnector(address));
server.addHandler(createHandler(runfiles, routes, filters));
server.addHandler(createHandler(runfiles, routes));
}
/** Starts the HTTP server in a new thread and returns once it's online. */
@ -156,10 +152,7 @@ public final class TestServer {
}
}
private Context createHandler(
Map<String, Path> runfiles,
ImmutableList<Route> routes,
ImmutableList<Class<? extends Filter>> filters) {
private Context createHandler(Map<String, Path> runfiles, ImmutableList<Route> routes) {
Context context = new Context(server, CONTEXT_PATH, Context.SESSIONS);
context.addServlet(new ServletHolder(HealthzServlet.class), "/healthz");
for (Map.Entry<String, Path> runfile : runfiles.entrySet()) {
@ -168,8 +161,7 @@ public final class TestServer {
runfile.getKey());
}
for (Route route : routes) {
context.addServlet(
new ServletHolder(wrapServlet(route.servletClass(), filters)), route.path());
context.addServlet(new ServletHolder(wrapServlet(route.servletClass())), route.path());
}
ServletHolder holder = new ServletHolder(DefaultServlet.class);
holder.setInitParameter("aliases", "1");
@ -177,9 +169,8 @@ public final class TestServer {
return context;
}
private HttpServlet wrapServlet(
Class<? extends HttpServlet> servletClass, ImmutableList<Class<? extends Filter>> filters) {
return new ServletWrapperDelegatorServlet(servletClass, filters, requestQueue);
private HttpServlet wrapServlet(Class<? extends HttpServlet> servletClass) {
return new ServletWrapperDelegatorServlet(servletClass, requestQueue);
}
private static Connector createConnector(HostAndPort address) {

View file

@ -39,9 +39,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.ofy.ObjectifyService;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarAddress;
@ -135,7 +132,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
private UserInfo userInfo;
// Test Objectify entity classes to be used with this AppEngineExtension instance.
private ImmutableList<Class<?>> ofyTestEntities;
private ImmutableList<Class<?>> jpaTestEntities;
public Optional<JpaIntegrationTestExtension> getJpaIntegrationTestExtension() {
@ -146,7 +142,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
public static class Builder {
private AppEngineExtension extension = new AppEngineExtension();
private ImmutableList.Builder<Class<?>> ofyTestEntities = new ImmutableList.Builder<>();
private ImmutableList.Builder<Class<?>> jpaTestEntities = new ImmutableList.Builder<>();
/** Turns on Cloud SQL only, for use by test data generators. */
@ -205,24 +200,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
return this;
}
/**
* Declares test-only entities to be registered with {@code ObjectifyService}.
*
* <p>Note that {@code ObjectifyService} silently replaces the current registration for a given
* kind when a different class is registered for this kind. Since {@code ObjectifyService} does
* not support de-registration, each test entity class must be of a unique kind across the
* entire code base. Although this requirement can be worked around by using different {@code
* ObjectifyService} instances for each test (class), the setup overhead would rise
* significantly.
*
* @see AppEngineExtension#register(Class)
*/
@SafeVarargs
public final Builder withOfyTestEntities(Class<?>... entities) {
ofyTestEntities.add(entities);
return this;
}
public Builder withJpaUnitTestEntities(Class<?>... entities) {
jpaTestEntities.add(entities);
extension.withJpaUnitTest = true;
@ -239,7 +216,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
checkState(
!extension.withJpaUnitTest || !extension.enableJpaEntityCoverageCheck,
"withJpaUnitTestEntities cannot be set when enableJpaEntityCoverageCheck");
extension.ofyTestEntities = this.ofyTestEntities.build();
extension.jpaTestEntities = this.jpaTestEntities.build();
return extension;
}
@ -440,9 +416,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
helper.setEnvInstance("0");
}
helper.setUp();
ObjectifyService.initOfy();
this.ofyTestEntities.forEach(AppEngineExtension::register);
}
/** Called after each test method. */
@ -472,7 +445,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
public void tearDown() throws Exception {
// Resets Objectify. Although it would seem more obvious to do this at the start of a request
// instead of at the end, this is more consistent with what ObjectifyFilter does in real code.
ObjectifyFilter.complete();
helper.tearDown();
helper = null;
// Test that Datastore didn't need any indexes we don't have listed in our index file.
@ -502,24 +474,6 @@ public final class AppEngineExtension implements BeforeEachCallback, AfterEachCa
}
}
/**
* Registers test-only Objectify entities and checks for re-registrations for the same kind by
* different classes.
*/
private static void register(Class<?> entityClass) {
String kind = Key.getKind(entityClass);
Optional.ofNullable(com.googlecode.objectify.ObjectifyService.factory().getMetadata(kind))
.ifPresent(
meta ->
checkState(
meta.getEntityClass() == entityClass,
"Cannot register %s. The Kind %s is already registered with %s.",
entityClass.getName(),
kind,
meta.getEntityClass().getName()));
com.googlecode.objectify.ObjectifyService.register(entityClass);
}
/** Install {@code testing/logging.properties} so logging is less noisy. */
private static void setupLogging() throws IOException {
LogManager.getLogManager()

View file

@ -15,27 +15,15 @@
package google.registry.testing;
import static com.google.common.io.Files.asCharSink;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.CollectionUtils.entriesToImmutableMap;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.base.Joiner;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.persistence.transaction.JpaTransactionManager;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -120,65 +108,7 @@ class AppEngineExtensionTest {
assertThrows(AssertionError.class, () -> appEngine.afterEach(context.getContext()));
}
@Test
void testRegisterOfyEntities_duplicateEntitiesWithSameName_fails() throws Exception {
AppEngineExtension appEngineExtension =
AppEngineExtension.builder()
.withCloudSql()
.withOfyTestEntities(google.registry.testing.TestObject.class, TestObject.class)
.build();
// Thrown before JPA is set up, therefore no need to call afterEach.
IllegalStateException thrown =
assertThrows(
IllegalStateException.class, () -> appEngineExtension.beforeEach(context.getContext()));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
String.format(
"Cannot register %s. The Kind %s is already registered with %s.",
TestObject.class.getName(),
"TestObject",
google.registry.testing.TestObject.class.getName()));
// The class level extension.
appEngine.afterEach(context.getContext());
}
@Test
void testOfyEntities_uniqueKinds() throws Exception {
try (ScanResult scanResult =
new ClassGraph()
.enableAnnotationInfo()
.ignoreClassVisibility()
.whitelistPackages("google.registry")
.scan()) {
Multimap<String, Class<?>> kindToEntityMultiMap =
scanResult.getClassesWithAnnotation(Entity.class.getName()).stream()
.filter(clazz -> !clazz.getName().equals(TestObject.class.getName()))
.map(clazz -> clazz.loadClass())
.collect(
Multimaps.toMultimap(
Key::getKind,
clazz -> clazz,
MultimapBuilder.hashKeys().linkedListValues()::build));
Map<String, Collection<Class<?>>> conflictingKinds =
kindToEntityMultiMap.asMap().entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.collect(entriesToImmutableMap());
assertWithMessage(
"Conflicting Ofy kinds found. Tests will break if they are registered with "
+ " AppEngineExtension in the same test executor.")
.that(conflictingKinds)
.isEmpty();
}
appEngine.afterEach(context.getContext());
}
private void writeAutoIndexFile(String content) throws IOException {
asCharSink(new File(appEngine.tmpDir, "datastore-indexes-auto.xml"), UTF_8).write(content);
}
@Entity
private static final class TestObject {
@Id long id;
}
}

View file

@ -1,78 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.testing;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.VirtualEntity;
import google.registry.persistence.VKey;
/** A test model object that can be persisted in any entity group. */
@DeleteAfterMigration
@Entity
public class TestObject extends ImmutableObject {
@Id @javax.persistence.Id String id;
String field;
public String getId() {
return id;
}
public String getField() {
return field;
}
public VKey<TestObject> key() {
return VKey.create(TestObject.class, id);
}
public static TestObject create(String id) {
return create(id, null);
}
public static TestObject create(String id, String field) {
TestObject instance = new TestObject();
instance.id = id;
instance.field = field;
return instance;
}
/** A test @VirtualEntity model object, which should not be persisted. */
@Entity
@VirtualEntity
public static class TestVirtualObject extends ImmutableObject {
@Id String id;
/**
* Expose a factory method for testing saves of virtual entities; in real life this would never
* be needed for an actual @VirtualEntity.
*/
public static TestVirtualObject create(String id) {
TestVirtualObject instance = new TestVirtualObject();
instance.id = id;
return instance;
}
public static Key<TestVirtualObject> createKey(String id) {
return Key.create(TestVirtualObject.class, id);
}
}
}

View file

@ -1,123 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityTranslator;
import com.google.common.collect.ImmutableList;
import com.google.storage.onestore.v3.OnestoreEntity.EntityProto;
import com.google.storage.onestore.v3.OnestoreEntity.Property;
import google.registry.testing.AppEngineExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link EntityWrapper}. */
public final class EntityWrapperTest {
private static final String TEST_ENTITY_KIND = "TestEntity";
private static final int ARBITRARY_KEY_ID = 1001;
@RegisterExtension
public final AppEngineExtension appEngine = AppEngineExtension.builder().withCloudSql().build();
@Test
void testEquals() {
// Create an entity with a key and some properties.
Entity entity = new Entity(TEST_ENTITY_KIND, ARBITRARY_KEY_ID);
// Note that we need to specify these as long for property comparisons to work because that's
// how they are deserialized from protos.
entity.setProperty("eeny", 100L);
entity.setProperty("meeny", 200L);
entity.setProperty("miney", 300L);
EntityProto proto1 = EntityTranslator.convertToPb(entity);
EntityProto proto2 = EntityTranslator.convertToPb(entity);
// Reorder the property list of proto2 (the protobuf stores this as a repeated field, so
// we just have to clear and re-add them in a different order).
ImmutableList<Property> properties =
ImmutableList.of(proto2.getProperty(2), proto2.getProperty(0), proto2.getProperty(1));
proto2.clearProperty();
for (Property property : properties) {
proto2.addProperty(property);
}
// Construct entity objects from the two protos.
Entity e1 = EntityTranslator.createFromPb(proto1);
Entity e2 = EntityTranslator.createFromPb(proto2);
// Ensure that we have a normalized representation.
EntityWrapper ce1 = new EntityWrapper(e1);
EntityWrapper ce2 = new EntityWrapper(e2);
assertThat(ce1).isEqualTo(ce2);
assertThat(ce1.hashCode()).isEqualTo(ce2.hashCode());
// Ensure that the original entity is equal.
assertThat(new EntityWrapper(entity)).isEqualTo(ce1);
}
@Test
void testDifferentPropertiesNotEqual() {
Entity entity = new Entity(TEST_ENTITY_KIND, ARBITRARY_KEY_ID);
// Note that we need to specify these as long for property comparisons to work because that's
// how they are deserialized from protos.
entity.setProperty("eeny", 100L);
entity.setProperty("meeny", 200L);
entity.setProperty("miney", 300L);
EntityProto proto1 = EntityTranslator.convertToPb(entity);
entity.setProperty("tiger!", 400);
EntityProto proto2 = EntityTranslator.convertToPb(entity);
// Construct entity objects from the two protos.
Entity e1 = EntityTranslator.createFromPb(proto1);
Entity e2 = EntityTranslator.createFromPb(proto2);
EntityWrapper ce1 = new EntityWrapper(e1);
EntityWrapper ce2 = new EntityWrapper(e2);
assertThat(e1).isEqualTo(e2); // The keys should still be the same.
assertThat(ce1).isNotEqualTo(ce2);
assertThat(ce1.hashCode()).isNotEqualTo(ce2.hashCode());
}
@Test
void testDifferentKeysNotEqual() {
EntityProto proto1 =
EntityTranslator.convertToPb(new Entity(TEST_ENTITY_KIND, ARBITRARY_KEY_ID));
EntityProto proto2 =
EntityTranslator.convertToPb(new Entity(TEST_ENTITY_KIND, ARBITRARY_KEY_ID + 1));
// Construct entity objects from the two protos.
Entity e1 = EntityTranslator.createFromPb(proto1);
Entity e2 = EntityTranslator.createFromPb(proto2);
EntityWrapper ce1 = new EntityWrapper(e1);
EntityWrapper ce2 = new EntityWrapper(e2);
assertThat(ce1).isNotEqualTo(ce2);
assertThat(ce1.hashCode()).isNotEqualTo(ce2.hashCode());
}
@Test
void testComparisonAgainstNonComparableEntities() {
EntityWrapper ce = new EntityWrapper(new Entity(TEST_ENTITY_KIND, ARBITRARY_KEY_ID));
// Note: this has to be "isNotEqualTo()" and not isNotNull() because we want to test the
// equals() method and isNotNull() just checks for "ce != null".
assertThat(ce).isNotEqualTo(null);
assertThat(ce).isNotEqualTo(new Object());
}
}

View file

@ -17,8 +17,6 @@ package google.registry.webdriver;
import static google.registry.server.Fixture.BASIC;
import static google.registry.server.Route.route;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.ofy.OfyFilter;
import google.registry.module.frontend.FrontendServlet;
import google.registry.server.RegistryTestServer;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -33,7 +31,6 @@ public class OteSetupConsoleScreenshotTest extends WebDriverTestCase {
new TestServerExtension.Builder()
.setRunfiles(RegistryTestServer.RUNFILES)
.setRoutes(route("/registrar-ote-setup", FrontendServlet.class))
.setFilters(ObjectifyFilter.class, OfyFilter.class)
.setFixtures(BASIC)
.setEmail("Marla.Singer@google.com")
.build();

View file

@ -28,10 +28,8 @@ import static google.registry.tools.LockOrUnlockDomainCommand.REGISTRY_LOCK_STAT
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.ofy.OfyFilter;
import google.registry.model.registrar.Registrar.State;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.module.frontend.FrontendServlet;
@ -59,7 +57,6 @@ class RegistrarConsoleScreenshotTest extends WebDriverTestCase {
route("/registrar-settings", FrontendServlet.class),
route("/registry-lock-get", FrontendServlet.class),
route("/registry-lock-verify", FrontendServlet.class))
.setFilters(ObjectifyFilter.class, OfyFilter.class)
.setFixtures(BASIC)
.setEmail("Marla.Singer@crr.com") // from AppEngineExtension.makeRegistrarContact3
.build();

View file

@ -21,8 +21,6 @@ import static google.registry.testing.DatabaseHelper.loadRegistrar;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.ofy.OfyFilter;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
@ -43,7 +41,6 @@ public class RegistrarConsoleWebTest extends WebDriverTestCase {
.setRoutes(
route("/registrar", FrontendServlet.class),
route("/registrar-settings", FrontendServlet.class))
.setFilters(ObjectifyFilter.class, OfyFilter.class)
.setFixtures(BASIC)
.setEmail("Marla.Singer@crr.com")
.build();

View file

@ -17,8 +17,6 @@ package google.registry.webdriver;
import static google.registry.server.Fixture.BASIC;
import static google.registry.server.Route.route;
import com.googlecode.objectify.ObjectifyFilter;
import google.registry.model.ofy.OfyFilter;
import google.registry.module.frontend.FrontendServlet;
import google.registry.server.RegistryTestServer;
import org.junit.jupiter.api.extension.RegisterExtension;
@ -33,7 +31,6 @@ class RegistrarCreateConsoleScreenshotTest extends WebDriverTestCase {
new TestServerExtension.Builder()
.setRunfiles(RegistryTestServer.RUNFILES)
.setRoutes(route("/registrar-create", FrontendServlet.class))
.setFilters(ObjectifyFilter.class, OfyFilter.class)
.setFixtures(BASIC)
.setEmail("Marla.Singer@google.com")
.build();

View file

@ -37,7 +37,6 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingDeque;
import javax.servlet.Filter;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
@ -59,7 +58,6 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC
private final BlockingQueue<FutureTask<?>> jobs = new LinkedBlockingDeque<>();
private final ImmutableMap<String, Path> runfiles;
private final ImmutableList<Route> routes;
private final ImmutableList<Class<? extends Filter>> filters;
private TestServer testServer;
private Thread serverThread;
@ -67,12 +65,10 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC
private TestServerExtension(
ImmutableMap<String, Path> runfiles,
ImmutableList<Route> routes,
ImmutableList<Class<? extends Filter>> filters,
ImmutableList<Fixture> fixtures,
String email) {
this.runfiles = runfiles;
this.routes = routes;
this.filters = filters;
this.fixtures = fixtures;
// We create an GAE-Admin user, and then use AuthenticatedRegistrarAccessor.bypassAdminCheck to
// choose whether the user is an admin or not.
@ -96,8 +92,7 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC
// can access this server.
getExternalAddressOfLocalSystem().getHostAddress(), pickUnusedPort()),
runfiles,
routes,
filters);
routes);
} catch (UnknownHostException e) {
throw new IllegalStateException(e);
}
@ -239,14 +234,12 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC
/**
* Builder for {@link TestServerExtension}.
*
* <p>This builder has three required fields: {@link #setRunfiles}, {@link #setRoutes}, and {@link
* #setFilters}.
* <p>This builder has two required fields: {@link #setRunfiles} and {@link #setRoutes}.
*/
public static final class Builder {
private ImmutableMap<String, Path> runfiles;
private ImmutableList<Route> routes;
ImmutableList<Class<? extends Filter>> filters;
private ImmutableList<Fixture> fixtures = ImmutableList.of();
private String email;
@ -263,13 +256,6 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC
return this;
}
/** Sets the list of servlet {@link Filter} objects for {@link TestServer}. */
@SafeVarargs
public final Builder setFilters(Class<? extends Filter>... filters) {
this.filters = ImmutableList.copyOf(filters);
return this;
}
/** Sets an ordered list of Datastore fixtures that should be loaded on startup. */
public Builder setFixtures(Fixture... fixtures) {
this.fixtures = ImmutableList.copyOf(fixtures);
@ -291,7 +277,6 @@ public final class TestServerExtension implements BeforeEachCallback, AfterEachC
return new TestServerExtension(
checkNotNull(this.runfiles),
checkNotNull(this.routes),
checkNotNull(this.filters),
checkNotNull(this.fixtures),
checkNotNull(this.email));
}

View file

@ -49,7 +49,6 @@ also defined:
incrementally.
* `python` -- Some Python reporting scripts
* `release` -- Configuration for our continuous integration process.
* `third_party` -- External dependencies.
## Build the codebase

View file

@ -19,11 +19,6 @@ plugins {
dependencies {
def deps = rootProject.dependencyMap
// Custom-built objectify jar at commit ecd5165, included in Nomulus
// release.
implementation files(
"${rootDir}/third_party/objectify/v4_1/objectify-4.1.3.jar")
implementation deps['com.google.code.findbugs:jsr305']
implementation deps['com.google.guava:guava']
implementation deps['com.squareup:javapoet']

View file

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2009-2013
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,19 +0,0 @@
This library includes custom serializers for AppEngine classes in
com.google.appengine.api.* packages. This is necessary because serializers
are discovered by AppEngine using a naming pattern that requires that the
data and serializer classes be in the same package (similar to how the Java
Beans introspector finds BeanInfo classes).
In Objectify versions 4.1 and later, the GWT emulation classes were broken
out into a separate versioned jar. Since we are jarjar repackaging the core
Objectify library to include a version number in the package, we need to
include the GWT files in this folder and apply the same changes to them.
The specific patches are:
* Fix ofy().load().fromEntity(...) to respect @OnLoad callbacks.
* Add Session.getKeys() to enumerate everything read in a session.
These changes are already in upstream, but no 4.x release has been made
that incorporates them. Therefore we need to backport them and vendor the
Objectify libarary here.

Binary file not shown.

View file

@ -57,7 +57,6 @@ dependencies {
testImplementation deps['org.mockito:mockito-core']
testImplementation deps['org.mockito:mockito-junit-jupiter']
testImplementation deps['org.testcontainers:junit-jupiter']
testImplementation files("${rootDir}/third_party/objectify/v4_1/objectify-4.1.3.jar")
testImplementation project(path: ':common', configuration: 'testing')
testRuntimeOnly deps['com.google.flogger:flogger-system-backend']
annotationProcessor deps['com.google.auto.value:auto-value']

View file

@ -1,82 +0,0 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.util;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
/** A placeholder GAE environment class that is used when masquerading a thread as a GAE thread. */
public final class PlaceholderEnvironment implements Environment {
private static final PlaceholderEnvironment INSTANCE = new PlaceholderEnvironment();
public static PlaceholderEnvironment get() {
return INSTANCE;
}
private PlaceholderEnvironment() {}
@Override
public String getAppId() {
return "PlaceholderAppId";
}
@Override
public Map<String, Object> getAttributes() {
return ImmutableMap.of();
}
@Override
public String getModuleId() {
throw new UnsupportedOperationException();
}
@Override
public String getVersionId() {
throw new UnsupportedOperationException();
}
@Override
public String getEmail() {
throw new UnsupportedOperationException();
}
@Override
public boolean isLoggedIn() {
throw new UnsupportedOperationException();
}
@Override
public boolean isAdmin() {
throw new UnsupportedOperationException();
}
@Override
public String getAuthDomain() {
throw new UnsupportedOperationException();
}
@SuppressWarnings("deprecation")
@Override
public String getRequestNamespace() {
throw new UnsupportedOperationException();
}
@Override
public long getRemainingMillis() {
throw new UnsupportedOperationException();
}
}