diff --git a/java/google/registry/config/ConfigModule.java b/java/google/registry/config/ConfigModule.java index 2edcb4587..fe3cafa4b 100644 --- a/java/google/registry/config/ConfigModule.java +++ b/java/google/registry/config/ConfigModule.java @@ -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"; + } } diff --git a/java/google/registry/flows/FlowComponent.java b/java/google/registry/flows/FlowComponent.java index 08c2d8f31..929792907 100644 --- a/java/google/registry/flows/FlowComponent.java +++ b/java/google/registry/flows/FlowComponent.java @@ -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, diff --git a/java/google/registry/flows/FlowUtils.java b/java/google/registry/flows/FlowUtils.java index c21d926c6..f9e9fbb94 100644 --- a/java/google/registry/flows/FlowUtils.java +++ b/java/google/registry/flows/FlowUtils.java @@ -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() { diff --git a/java/google/registry/flows/custom/BaseFlowCustomLogic.java b/java/google/registry/flows/custom/BaseFlowCustomLogic.java new file mode 100644 index 000000000..fcec87ab2 --- /dev/null +++ b/java/google/registry/flows/custom/BaseFlowCustomLogic.java @@ -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; + } +} diff --git a/java/google/registry/flows/custom/CustomLogicFactory.java b/java/google/registry/flows/custom/CustomLogicFactory.java new file mode 100644 index 000000000..b32dbde00 --- /dev/null +++ b/java/google/registry/flows/custom/CustomLogicFactory.java @@ -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. + * + *

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); + } +} diff --git a/java/google/registry/flows/custom/CustomLogicFactoryModule.java b/java/google/registry/flows/custom/CustomLogicFactoryModule.java new file mode 100644 index 000000000..050c04011 --- /dev/null +++ b/java/google/registry/flows/custom/CustomLogicFactoryModule.java @@ -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)); + } +} diff --git a/java/google/registry/flows/custom/CustomLogicModule.java b/java/google/registry/flows/custom/CustomLogicModule.java new file mode 100644 index 000000000..dd2f9f02b --- /dev/null +++ b/java/google/registry/flows/custom/CustomLogicModule.java @@ -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); + } +} diff --git a/java/google/registry/flows/custom/DomainCreateFlowCustomLogic.java b/java/google/registry/flows/custom/DomainCreateFlowCustomLogic.java new file mode 100644 index 000000000..63da19ed2 --- /dev/null +++ b/java/google/registry/flows/custom/DomainCreateFlowCustomLogic.java @@ -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. + * + *

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. + * + *

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(); + } + } +} diff --git a/java/google/registry/flows/custom/EntityChanges.java b/java/google/registry/flows/custom/EntityChanges.java new file mode 100644 index 000000000..0e9776f54 --- /dev/null +++ b/java/google/registry/flows/custom/EntityChanges.java @@ -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 getSaves(); + + public abstract ImmutableSet> 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.of()) + .setDeletes(ImmutableSet.>of()); + } + + /** Builder for {@link EntityChanges}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setSaves(ImmutableSet entitiesToSave); + + public abstract ImmutableSet.Builder savesBuilder(); + + public Builder addSave(ImmutableObject entityToSave) { + savesBuilder().add(entityToSave); + return this; + } + + public abstract Builder setDeletes(ImmutableSet> entitiesToDelete); + + public abstract ImmutableSet.Builder> deletesBuilder(); + + public Builder addDelete(Key entityToDelete) { + deletesBuilder().add(entityToDelete); + return this; + } + + public abstract EntityChanges build(); + } +} diff --git a/java/google/registry/flows/domain/DomainCreateFlow.java b/java/google/registry/flows/domain/DomainCreateFlow.java index 7bab600f8..520e38fc7 100644 --- a/java/google/registry/flows/domain/DomainCreateFlow.java +++ b/java/google/registry/flows/domain/DomainCreateFlow.java @@ -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 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 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()) { diff --git a/java/google/registry/module/frontend/FrontendComponent.java b/java/google/registry/module/frontend/FrontendComponent.java index 45015bdc0..22064c160 100644 --- a/java/google/registry/module/frontend/FrontendComponent.java +++ b/java/google/registry/module/frontend/FrontendComponent.java @@ -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, diff --git a/java/google/registry/module/tools/ToolsComponent.java b/java/google/registry/module/tools/ToolsComponent.java index 7fd5c54d0..a8fd8f27f 100644 --- a/java/google/registry/module/tools/ToolsComponent.java +++ b/java/google/registry/module/tools/ToolsComponent.java @@ -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, diff --git a/java/google/registry/util/TypeUtils.java b/java/google/registry/util/TypeUtils.java index 86aa0655b..c9cda6551 100644 --- a/java/google/registry/util/TypeUtils.java +++ b/java/google/registry/util/TypeUtils.java @@ -56,6 +56,28 @@ public class TypeUtils { } } + /** + * Returns the class referred to by a fully qualified class name string. + * + *

Throws an error if the loaded class is not assignable from the expected super type class. + */ + public static Class getClassFromString(String className, Class 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 castedClass = (Class) clazz; + return castedClass; + } + /** * Aggregates enum "values" in a typesafe enum pattern into a string->field map. */ diff --git a/javatests/google/registry/flows/EppTestComponent.java b/javatests/google/registry/flows/EppTestComponent.java index a312ca65d..e53b93d84 100644 --- a/javatests/google/registry/flows/EppTestComponent.java +++ b/javatests/google/registry/flows/EppTestComponent.java @@ -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 { diff --git a/javatests/google/registry/util/TypeUtilsTest.java b/javatests/google/registry/util/TypeUtilsTest.java new file mode 100644 index 000000000..9420ab77d --- /dev/null +++ b/javatests/google/registry/util/TypeUtilsTest.java @@ -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 clazz = + TypeUtils.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.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.getClassFromString("com.fake.company.nonexistent.Class", Object.class); + } +}