Add a custom logic framework to provide pluggable extensibility

To add additional logic for flow code, write custom classes that extend the existing custom logic classes (of which DomainCreateFlowCustomLogic is the first provided example), along with a class that extends CustomLogicFactory to provide instances of the new custom logic classes. Then configure the fully qualified class name of your new custom logic factory in ConfigModule.provideCustomLogicFactoryClass().

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=139221577
This commit is contained in:
mcilwain 2016-11-15 11:01:10 -08:00 committed by Ben McIlwain
parent 3942f0768b
commit 4d2e0941f3
15 changed files with 435 additions and 17 deletions

View file

@ -914,4 +914,11 @@ public final class ConfigModule {
public static String provideGreetingServerId() {
return "Charleston Road Registry";
}
@Provides
@Config("customLogicFactoryClass")
public static String provideCustomLogicFactoryClass() {
// TODO(b/32875427): This will be moved into configuration in a text file in a future refactor.
return "google.registry.flows.custom.CustomLogicFactory";
}
}

View file

@ -30,6 +30,7 @@ import google.registry.flows.contact.ContactTransferQueryFlow;
import google.registry.flows.contact.ContactTransferRejectFlow;
import google.registry.flows.contact.ContactTransferRequestFlow;
import google.registry.flows.contact.ContactUpdateFlow;
import google.registry.flows.custom.CustomLogicModule;
import google.registry.flows.domain.ClaimsCheckFlow;
import google.registry.flows.domain.DomainAllocateFlow;
import google.registry.flows.domain.DomainApplicationCreateFlow;
@ -66,6 +67,7 @@ import google.registry.util.SystemSleeper.SystemSleeperModule;
@Subcomponent(modules = {
AsyncFlowsModule.class,
ConfigModule.class,
CustomLogicModule.class,
DnsModule.class,
FlowModule.class,
FlowComponent.FlowComponentModule.class,

View file

@ -14,7 +14,10 @@
package google.registry.flows;
import static google.registry.model.ofy.ObjectifyService.ofy;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.custom.EntityChanges;
/** Static utility functions for flows. */
public final class FlowUtils {
@ -28,6 +31,12 @@ public final class FlowUtils {
}
}
/** Persists the saves and deletes in an {@link EntityChanges} to Datastore. */
public static void persistEntityChanges(EntityChanges entityChanges) {
ofy().save().entities(entityChanges.getSaves());
ofy().delete().keys(entityChanges.getDeletes());
}
/** Registrar is not logged in. */
public static class NotLoggedInException extends CommandUseErrorException {
public NotLoggedInException() {

View file

@ -0,0 +1,41 @@
// Copyright 2016 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.flows.custom;
import google.registry.flows.SessionMetadata;
import google.registry.model.eppinput.EppInput;
/**
* An abstract base class for all flow custom logic that stores the flow's {@link EppInput} and
* {@link SessionMetadata} for convenience. Both of these are immutable.
*/
public abstract class BaseFlowCustomLogic {
private final EppInput eppInput;
private final SessionMetadata sessionMetadata;
protected BaseFlowCustomLogic(EppInput eppInput, SessionMetadata sessionMetadata) {
this.eppInput = eppInput;
this.sessionMetadata = sessionMetadata;
}
protected EppInput getEppInput() {
return eppInput;
}
protected SessionMetadata getSessionMetadata() {
return sessionMetadata;
}
}

View file

@ -0,0 +1,38 @@
// Copyright 2016 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.flows.custom;
import google.registry.config.ConfigModule;
import google.registry.flows.SessionMetadata;
import google.registry.model.eppinput.EppInput;
/**
* A no-op base custom logic factory.
*
* <p>To add custom logic, extend this class, then configure it in
* {@link ConfigModule#provideCustomLogicFactoryClass}. The eppInput and sessionMetadata parameters
* are unused in the base implementation, but are provided so that custom implementations can
* optionally determine how to construct/choose which custom logic class to return. A common use
* case might be parsing TLD for domain-specific flows from the EppInput and then using that to
* choose a different custom logic implementation, or switching based on the registrar
* {@code clientId} in sessionMetadata.
*/
public class CustomLogicFactory {
public DomainCreateFlowCustomLogic forDomainCreateFlow(
EppInput eppInput, SessionMetadata sessionMetadata) {
return new DomainCreateFlowCustomLogic(eppInput, sessionMetadata);
}
}

View file

@ -0,0 +1,33 @@
// Copyright 2016 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.flows.custom;
import static google.registry.util.TypeUtils.getClassFromString;
import static google.registry.util.TypeUtils.instantiate;
import dagger.Module;
import dagger.Provides;
import google.registry.config.ConfigModule.Config;
/** Dagger module for custom logic factories. */
@Module
public class CustomLogicFactoryModule {
@Provides
static CustomLogicFactory provideCustomLogicFactory(
@Config("customLogicFactoryClass") String factoryClass) {
return instantiate(getClassFromString(factoryClass, CustomLogicFactory.class));
}
}

View file

@ -0,0 +1,31 @@
// Copyright 2016 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.flows.custom;
import dagger.Module;
import dagger.Provides;
import google.registry.flows.SessionMetadata;
import google.registry.model.eppinput.EppInput;
/** Dagger module to provide instances of custom logic classes for EPP flows. */
@Module
public class CustomLogicModule {
@Provides
static DomainCreateFlowCustomLogic provideDomainCreateFlowCustomLogic(
CustomLogicFactory factory, EppInput eppInput, SessionMetadata sessionMetadata) {
return factory.forDomainCreateFlow(eppInput, sessionMetadata);
}
}

View file

@ -0,0 +1,98 @@
// Copyright 2016 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.flows.custom;
import com.google.auto.value.AutoValue;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.SessionMetadata;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainResource;
import google.registry.model.eppinput.EppInput;
import google.registry.model.reporting.HistoryEntry;
/**
* A no-op base class for domain create flow custom logic.
*
* <p>Extend this class and override the hooks to perform custom logic.
*/
public class DomainCreateFlowCustomLogic extends BaseFlowCustomLogic {
protected DomainCreateFlowCustomLogic(EppInput eppInput, SessionMetadata sessionMetadata) {
super(eppInput, sessionMetadata);
}
/** A hook that runs at the end of the validation step to perform additional validation. */
@SuppressWarnings("unused")
public void afterValidation(AfterValidationParameters parameters) throws EppException {
// Do nothing.
}
/**
* A hook that runs before new entities are persisted.
*
* <p>This takes the new entities as input and returns the actual entities to save. It is
* important to be careful when changing the flow behavior for existing entities, because the core
* logic across many different flows expects the existence of these entities and many of the
* fields on them.
*/
@SuppressWarnings("unused")
public EntityChanges beforeSave(BeforeSaveParameters parameters, EntityChanges entityChanges)
throws EppException {
return entityChanges;
}
/** A class to encapsulate parameters for a call to {@link #afterValidation}. */
@AutoValue
public abstract static class AfterValidationParameters extends ImmutableObject {
public abstract InternetDomainName domainName();
public abstract int years();
public static Builder newBuilder() {
return new AutoValue_DomainCreateFlowCustomLogic_AfterValidationParameters.Builder();
}
/** Builder for {@link AfterValidationParameters}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setDomainName(InternetDomainName domainName);
public abstract Builder setYears(int years);
public abstract AfterValidationParameters build();
}
}
/** A class to encapsulate parameters for a call to {@link #beforeSave}. */
@AutoValue
public abstract static class BeforeSaveParameters extends ImmutableObject {
public abstract DomainResource newDomain();
public abstract HistoryEntry historyEntry();
public abstract int years();
public static Builder newBuilder() {
return new AutoValue_DomainCreateFlowCustomLogic_BeforeSaveParameters.Builder();
}
/** Builder for {@link BeforeSaveParameters}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setNewDomain(DomainResource newDomain);
public abstract Builder setHistoryEntry(HistoryEntry historyEntry);
public abstract Builder setYears(int years);
public abstract BeforeSaveParameters build();
}
}
}

View file

@ -0,0 +1,62 @@
// Copyright 2016 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.flows.custom;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
/** A wrapper class that encapsulates Datastore entities to both save and delete. */
@AutoValue
public abstract class EntityChanges {
public abstract ImmutableSet<ImmutableObject> getSaves();
public abstract ImmutableSet<Key<ImmutableObject>> getDeletes();
public static Builder newBuilder() {
// Default both entities to save and entities to delete to empty sets, so that the build()
// method won't subsequently throw an exception if one doesn't end up being applicable.
return new AutoValue_EntityChanges.Builder()
.setSaves(ImmutableSet.<ImmutableObject>of())
.setDeletes(ImmutableSet.<Key<ImmutableObject>>of());
}
/** Builder for {@link EntityChanges}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setSaves(ImmutableSet<ImmutableObject> entitiesToSave);
public abstract ImmutableSet.Builder<ImmutableObject> savesBuilder();
public Builder addSave(ImmutableObject entityToSave) {
savesBuilder().add(entityToSave);
return this;
}
public abstract Builder setDeletes(ImmutableSet<Key<ImmutableObject>> entitiesToDelete);
public abstract ImmutableSet.Builder<Key<ImmutableObject>> deletesBuilder();
public Builder addDelete(Key<ImmutableObject> entityToDelete) {
deletesBuilder().add(entityToDelete);
return this;
}
public abstract EntityChanges build();
}
}

View file

@ -14,6 +14,7 @@
package google.registry.flows.domain;
import static google.registry.flows.FlowUtils.persistEntityChanges;
import static google.registry.flows.FlowUtils.validateClientIsLoggedIn;
import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
@ -57,6 +58,8 @@ import google.registry.flows.FlowModule.ClientId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.custom.DomainCreateFlowCustomLogic;
import google.registry.flows.custom.EntityChanges;
import google.registry.flows.domain.TldSpecificLogicProxy.EppCommandOperations;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
@ -162,6 +165,7 @@ public class DomainCreateFlow implements TransactionalFlow {
@Inject @Superuser boolean isSuperuser;
@Inject HistoryEntry.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainCreateFlowCustomLogic customLogic;
@Inject DomainCreateFlow() {}
@Override
@ -198,6 +202,12 @@ public class DomainCreateFlow implements TransactionalFlow {
if (hasSignedMarks) {
verifySignedMarksAllowed(tldState, isAnchorTenant);
}
customLogic.afterValidation(
DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder()
.setDomainName(domainName)
.setYears(years)
.build());
FeeCreateCommandExtension feeCreate =
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
EppCommandOperations commandOperations = TldSpecificLogicProxy.getCreatePrice(
@ -269,7 +279,6 @@ public class DomainCreateFlow implements TransactionalFlow {
.setContacts(command.getContacts())
.addGracePeriod(GracePeriod.forBillingEvent(GracePeriodStatus.ADD, createBillingEvent))
.build();
handleExtraFlowLogic(registry.getTldStr(), years, historyEntry, newDomain);
entitiesToSave.add(
newDomain,
ForeignKeyIndex.create(newDomain, newDomain.getDeletionTime()),
@ -282,7 +291,29 @@ public class DomainCreateFlow implements TransactionalFlow {
prepareMarkedLrpTokenEntity(authInfo.getPw().getValue(), domainName, historyEntry));
}
enqueueTasks(hasSignedMarks, hasClaimsNotice, newDomain);
ofy().save().entities(entitiesToSave.build());
// TODO: Remove this section and only use the customLogic.
Optional<RegistryExtraFlowLogic> extraFlowLogic =
RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr());
if (extraFlowLogic.isPresent()) {
extraFlowLogic.get().performAdditionalDomainCreateLogic(
newDomain,
clientId,
years,
eppInput,
historyEntry);
}
EntityChanges entityChanges =
customLogic.beforeSave(
DomainCreateFlowCustomLogic.BeforeSaveParameters.newBuilder()
.setNewDomain(newDomain)
.setHistoryEntry(historyEntry)
.setYears(years)
.build(),
EntityChanges.newBuilder().setSaves(entitiesToSave.build()).build());
persistEntityChanges(entityChanges);
return responseBuilder
.setResData(DomainCreateData.create(targetId, now, registrationExpirationTime))
.setExtensions(createResponseExtensions(feeCreate, commandOperations))
@ -395,21 +426,6 @@ public class DomainCreateFlow implements TransactionalFlow {
return registry.getLrpPeriod().contains(now) && !isAnchorTenant;
}
private void handleExtraFlowLogic(
String tld, int years, HistoryEntry historyEntry, DomainResource newDomain)
throws EppException {
Optional<RegistryExtraFlowLogic> extraFlowLogic =
RegistryExtraFlowLogicProxy.newInstanceForTld(tld);
if (extraFlowLogic.isPresent()) {
extraFlowLogic.get().performAdditionalDomainCreateLogic(
newDomain,
clientId,
years,
eppInput,
historyEntry);
}
}
private void enqueueTasks(
boolean hasSignedMarks, boolean hasClaimsNotice, DomainResource newDomain) {
if (newDomain.shouldPublishToDns()) {

View file

@ -17,6 +17,7 @@ package google.registry.module.frontend;
import dagger.Component;
import google.registry.braintree.BraintreeModule;
import google.registry.config.ConfigModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.keyring.api.DummyKeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.module.frontend.FrontendRequestComponent.FrontendRequestComponentModule;
@ -40,6 +41,7 @@ import javax.inject.Singleton;
BraintreeModule.class,
ConfigModule.class,
ConsoleConfigModule.class,
CustomLogicFactoryModule.class,
DummyKeyringModule.class,
FrontendMetricsModule.class,
FrontendRequestComponentModule.class,

View file

@ -17,6 +17,7 @@ package google.registry.module.tools;
import dagger.Component;
import google.registry.config.ConfigModule;
import google.registry.export.DriveModule;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.gcs.GcsServiceModule;
import google.registry.groups.DirectoryModule;
import google.registry.groups.GroupsModule;
@ -42,6 +43,7 @@ import javax.inject.Singleton;
modules = {
AppIdentityCredentialModule.class,
ConfigModule.class,
CustomLogicFactoryModule.class,
DatastoreServiceModule.class,
DirectoryModule.class,
DriveModule.class,

View file

@ -56,6 +56,28 @@ public class TypeUtils {
}
}
/**
* Returns the class referred to by a fully qualified class name string.
*
* <p>Throws an error if the loaded class is not assignable from the expected super type class.
*/
public static <T> Class<T> getClassFromString(String className, Class<T> expectedSuperType) {
Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(String.format("Failed to load class %s", className), e);
}
checkArgument(
expectedSuperType.isAssignableFrom(clazz),
"%s does not implement/extend %s",
clazz.getSimpleName(),
expectedSuperType.getSimpleName());
@SuppressWarnings("unchecked")
Class<T> castedClass = (Class<T>) clazz;
return castedClass;
}
/**
* Aggregates enum "values" in a typesafe enum pattern into a string->field map.
*/

View file

@ -23,6 +23,7 @@ import dagger.Provides;
import dagger.Subcomponent;
import google.registry.config.ConfigModule;
import google.registry.dns.DnsQueue;
import google.registry.flows.custom.CustomLogicFactoryModule;
import google.registry.monitoring.whitebox.BigQueryMetricsEnqueuer;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.request.RequestScope;
@ -35,6 +36,7 @@ import javax.inject.Singleton;
@Component(
modules = {
ConfigModule.class,
CustomLogicFactoryModule.class,
EppTestComponent.FakesAndMocksModule.class
})
interface EppTestComponent {

View file

@ -0,0 +1,53 @@
// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.util;
import static com.google.common.truth.Truth.assertThat;
import google.registry.testing.ExceptionRule;
import java.io.Serializable;
import java.util.ArrayList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link TypeUtils}. */
@RunWith(JUnit4.class)
public class TypeUtilsTest {
@Rule
public final ExceptionRule thrown = new ExceptionRule();
@Test
public void test_getClassFromString_validClass() {
Class<? extends Serializable> clazz =
TypeUtils.<Serializable>getClassFromString("java.util.ArrayList", Serializable.class);
assertThat(clazz).isEqualTo(ArrayList.class);
}
@Test
public void test_getClassFromString_notAssignableFrom() {
thrown.expect(IllegalArgumentException.class, "ArrayList does not implement/extend Integer");
TypeUtils.<Integer>getClassFromString("java.util.ArrayList", Integer.class);
}
@Test
public void test_getClassFromString_unknownClass() {
thrown.expect(
IllegalArgumentException.class, "Failed to load class com.fake.company.nonexistent.Class");
TypeUtils.<Object>getClassFromString("com.fake.company.nonexistent.Class", Object.class);
}
}