diff --git a/resource/src/main/java/org/creekservice/api/platform/resource/ResourceInitializer.java b/resource/src/main/java/org/creekservice/api/platform/resource/ResourceInitializer.java index 7c7ea5a..7f6aeca 100644 --- a/resource/src/main/java/org/creekservice/api/platform/resource/ResourceInitializer.java +++ b/resource/src/main/java/org/creekservice/api/platform/resource/ResourceInitializer.java @@ -58,42 +58,58 @@ public final class ResourceInitializer { private static final StructuredLogger LOGGER = StructuredLoggerFactory.internalLogger(ResourceInitializer.class); - private final ResourceCreator resourceCreator; + private final Callbacks callbacks; private final ComponentValidator componentValidator; - /** Type for ensuring external resources exist */ - @FunctionalInterface - public interface ResourceCreator { + /** Type for supplying callbacks to the resource initializer. */ + public interface Callbacks { /** - * Get the handler for a specific type. + * Callback to validate that all the supplied {@code resources} consistently represent the + * same resource. + * + *

There is often multiple resource descriptors describing the same resource present in + * the system. For example, both input and output descriptors for the same resource. Any + * inconsistencies in the details of the resource between these descriptors can lead to + * bugs. + * + * @param type the type of the {@code resources}. + * @param resources the set of resources that all share the same {@link + * ResourceDescriptor#id()}. + * @param the resource descriptor type + * @throws RuntimeException with the details of any inconsistencies. + */ + void validate(Class type, Collection resources); + + /** + * Callback to ensure the supplied {@code creatableResources} exist. * *

All {@code creatableResources} will be of the same type. * + * @param type the type of the {@code creatableResources}. * @param creatableResources the resource instances to ensure exists and are initialized. * Resources passed will be {@link ResourceDescriptor#isCreatable creatable}. * @param the creatable resource descriptor type * @throws RuntimeException on unknown resource type */ void ensure( - Collection creatableResources); + Class type, Collection creatableResources); } /** * Create an initializer instance. * - * @param resourceCreator callback used to ensure external resources exist, as exposed by Creek - * extensions. + * @param callbacks callbacks used to ensure external resources exist and are valid, as exposed + * by Creek extensions. * @return an initializer instance. */ - public static ResourceInitializer resourceInitializer(final ResourceCreator resourceCreator) { - return new ResourceInitializer(resourceCreator, new ComponentValidator()); + public static ResourceInitializer resourceInitializer(final Callbacks callbacks) { + return new ResourceInitializer(callbacks, new ComponentValidator()); } @VisibleForTesting - ResourceInitializer( - final ResourceCreator resourceCreator, final ComponentValidator componentValidator) { - this.resourceCreator = requireNonNull(resourceCreator, "resourceCreator"); + ResourceInitializer(final Callbacks callbacks, final ComponentValidator componentValidator) { + this.callbacks = requireNonNull(callbacks, "callbacks"); this.componentValidator = requireNonNull(componentValidator, "componentValidator"); } @@ -113,8 +129,7 @@ public void init(final Collection components) { ensureResources( groupById( components, - resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isShared), - false)); + resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isShared))); } /** @@ -132,8 +147,7 @@ public void service(final Collection components) ensureResources( groupById( components, - resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isOwned), - true)); + resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isOwned))); } /** @@ -163,18 +177,15 @@ public void test( resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isUnowned) && resGroup.stream() - .noneMatch(ResourceDescriptor::isOwned), - true) + .noneMatch(ResourceDescriptor::isOwned)) .collect(Collectors.toMap(group -> group.get(0).id(), Function.identity())); groupById( otherComponents, - resGroup -> resGroup.stream().anyMatch(r -> unowned.containsKey(r.id())), - false) + resGroup -> resGroup.stream().anyMatch(r -> unowned.containsKey(r.id()))) .forEach(resGroup -> unowned.get(resGroup.get(0).id()).addAll(resGroup)); - final Stream> stream = unowned.values().stream(); - ensureResources(stream); + ensureResources(unowned.values().stream()); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -184,7 +195,11 @@ private void ensureResources(final Stream> resGroups) { .map(this::creatableDescriptor) .collect(groupingBy(Object::getClass)) .values() - .forEach(creatableResources -> resourceCreator.ensure((List) creatableResources)); + .forEach( + creatableResources -> + callbacks.ensure( + (Class) creatableResources.get(0).getClass(), + (List) creatableResources)); } private ResourceDescriptor creatableDescriptor(final List resGroup) { @@ -196,17 +211,12 @@ private ResourceDescriptor creatableDescriptor(final List re private Stream> groupById( final Collection components, - final Predicate> groupPredicate, - final boolean validate) { + final Predicate> groupPredicate) { final Map> grouped = components.stream() .flatMap(this::getResources) .collect(groupingBy(ResourceDescriptor::id)); - if (validate) { - grouped.values().forEach(this::validateResourceGroup); - } - return grouped.values().stream().filter(groupPredicate); } @@ -221,14 +231,16 @@ private Stream getResources(final ComponentDescriptor compon * * @param resourceGroup the group of descriptors that describe the same resource. */ + @SuppressWarnings("unchecked") private void validateResourceGroup(final List resourceGroup) { - if (isShared(resourceGroup.get(0))) { + final T first = resourceGroup.get(0); + if (isShared(first)) { // if shared, all should be shared. if (resourceGroup.stream().anyMatch(r -> !isShared(r))) { throw new ResourceDescriptorMismatchInitializationException( "shared", resourceGroup); } - } else if (isUnmanaged(resourceGroup.get(0))) { + } else if (isUnmanaged(first)) { // if unmanaged, all should be unmanaged. if (resourceGroup.stream().anyMatch(r -> !isUnmanaged(r))) { throw new ResourceDescriptorMismatchInitializationException( @@ -241,6 +253,8 @@ private void validateResourceGroup(final List "owned or unowned", resourceGroup); } } + + callbacks.validate((Class) first.getClass(), resourceGroup); } private static String formatResource(final List descriptors) { diff --git a/resource/src/test/java/org/creekservice/api/platform/resource/ResourceInitializerTest.java b/resource/src/test/java/org/creekservice/api/platform/resource/ResourceInitializerTest.java index d67532d..d5d8c4f 100644 --- a/resource/src/test/java/org/creekservice/api/platform/resource/ResourceInitializerTest.java +++ b/resource/src/test/java/org/creekservice/api/platform/resource/ResourceInitializerTest.java @@ -57,7 +57,7 @@ class ResourceInitializerTest { @Mock private ComponentValidator validator; @Mock private ComponentDescriptor component0; @Mock private ComponentDescriptor component1; - @Mock private ResourceInitializer.ResourceCreator resourceCreator; + @Mock private ResourceInitializer.Callbacks callbacks; @Mock(extraInterfaces = SharedResource.class) private ResourceA sharedResource1; @@ -74,7 +74,7 @@ class ResourceInitializerTest { @BeforeEach void setUp() { - initializer = new ResourceInitializer(resourceCreator, validator); + initializer = new ResourceInitializer(callbacks, validator); when(sharedResource1.id()).thenReturn(A1_ID); when(ownedResource1.id()).thenReturn(A1_ID); @@ -224,6 +224,76 @@ void shouldThrowIfResourceGroupContainsUnownedAndOther() { assertThat(e.getMessage(), containsString("sharedResource1")); } + @SuppressWarnings("unchecked") + @Test + void shouldCallbackValidateEachResGroupOnInit() { + // Given: + final ResourceA sharedResource2 = resourceA(1, SharedResource.class); + when(component0.resources()).thenReturn(Stream.of(sharedResource1)); + when(component1.resources()).thenReturn(Stream.of(sharedResource2)); + + // When: + initializer.init(List.of(component0, component1)); + + // Then: + verify(callbacks) + .validate( + (Class) sharedResource1.getClass(), + List.of(sharedResource1, sharedResource2)); + } + + @SuppressWarnings("unchecked") + @Test + void shouldCallbackValidateEachResGroupOnService() { + // Given: + when(component0.resources()).thenReturn(Stream.of(ownedResource1)); + when(component1.resources()).thenReturn(Stream.of(unownedResource1)); + + // When: + initializer.service(List.of(component0, component1)); + + // Then: + verify(callbacks) + .validate( + (Class) ownedResource1.getClass(), + List.of(ownedResource1, unownedResource1)); + } + + @SuppressWarnings("unchecked") + @Test + void shouldCallbackValidateEachResGroupOnTest() { + // Given: + when(component0.resources()).thenReturn(Stream.of(unownedResource1)); + when(component1.resources()).thenReturn(Stream.of(ownedResource1)); + + // When: + initializer.test(List.of(component0), List.of(component1)); + + // Then: + verify(callbacks) + .validate( + (Class) unownedResource1.getClass(), + List.of(unownedResource1, ownedResource1)); + } + + @Test + void shouldThrowIfValidateCallbackThrows() { + // Given: + when(component0.resources()).thenReturn(Stream.of(ownedResource1)); + when(component1.resources()).thenReturn(Stream.of(unownedResource1)); + final RuntimeException expected = new RuntimeException("BIG BADA BOOM"); + doThrow(expected).when(callbacks).validate(any(), any()); + + // When: + final Exception e = + assertThrows( + RuntimeException.class, + () -> initializer.service(List.of(component0, component1))); + + // Then: + assertThat(e, is(expected)); + } + @Test void shouldNotInitializeAnyResourceOnInitIfNoSharedResources() { // Given: @@ -234,7 +304,7 @@ void shouldNotInitializeAnyResourceOnInitIfNoSharedResources() { initializer.init(List.of(component0, component1)); // Then: - verify(resourceCreator, never()).ensure(any()); + verify(callbacks, never()).ensure(any(), any()); } @Test @@ -249,7 +319,7 @@ void shouldNotInitializeAnyResourcesOnServiceIfNoOwnedResources() { initializer.service(List.of(component0, component1)); // Then: - verify(resourceCreator, never()).ensure(any()); + verify(callbacks, never()).ensure(any(), any()); } @Test @@ -266,7 +336,7 @@ void shouldNotInitializeAnyResourcesOnTestIfNoUnownedResources() { initializer.test(List.of(component0), List.of(component1)); // Then: - verify(resourceCreator, never()).ensure(any()); + verify(callbacks, never()).ensure(any(), any()); } @Test @@ -280,7 +350,7 @@ void shouldNotInitializeAnyResourcesOnTestIfNoUnownedResources() { initializer.test(List.of(component0, component1), List.of()); // Then: - verify(resourceCreator, never()).ensure(any()); + verify(callbacks, never()).ensure(any(), any()); } @Test @@ -296,7 +366,7 @@ void shouldNotInitializeUnmanagedGroups() { initializer.init(List.of(component0, component1)); // Then: - verify(resourceCreator, never()).ensure(any()); + verify(callbacks, never()).ensure(any(), any()); } @Test @@ -332,7 +402,10 @@ void shouldEnsureSharedResource() { initializer.init(List.of(component0, component1)); // Then: - verify(resourceCreator).ensure((List) List.of(sharedResource1, sharedResource2)); + verify(callbacks) + .ensure( + (Class) sharedResource1.getClass(), + (List) List.of(sharedResource1, sharedResource2)); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -347,7 +420,10 @@ void shouldEnsureOwnedResource() { initializer.service(List.of(component0, component1)); // Then: - verify(resourceCreator).ensure((List) List.of(ownedResource1, ownedResource2)); + verify(callbacks) + .ensure( + (Class) ownedResource1.getClass(), + (List) List.of(ownedResource1, ownedResource2)); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -363,14 +439,14 @@ void shouldEnsureUnownedResource() { initializer.test(List.of(component0), List.of(component1)); // Then: - verify(resourceCreator).ensure((List) List.of(ownedResource2)); + verify(callbacks).ensure((Class) ownedResource2.getClass(), (List) List.of(ownedResource2)); } @Test void shouldThrowIfEnsureThrows() { // Given: final RuntimeException expected = new RuntimeException("boom"); - doThrow(expected).when(resourceCreator).ensure(any()); + doThrow(expected).when(callbacks).ensure(any(), any()); when(component0.resources()).thenReturn(Stream.of(ownedResource1)); // When: @@ -393,15 +469,15 @@ void shouldEnsureGroupingByHandler() { initializer.service(List.of(component0)); // Then: - verify(resourceCreator).ensure((List) List.of(ownedResource1)); - verify(resourceCreator).ensure((List) List.of(ownedResourceB)); + verify(callbacks).ensure((Class) ownedResource1.getClass(), (List) List.of(ownedResource1)); + verify(callbacks).ensure((Class) ownedResourceB.getClass(), (List) List.of(ownedResourceB)); } @Test void shouldThrowOnUnknownResourceType() { // Given: final NullPointerException expected = new NullPointerException("unknown"); - doThrow(expected).when(resourceCreator).ensure(any()); + doThrow(expected).when(callbacks).ensure(any(), any()); when(component0.resources()).thenReturn(Stream.of(sharedResource1)); // When: @@ -416,7 +492,7 @@ void shouldThrowOnUnknownResourceType() { @Test void shouldThrowOnInvalidComponentUsingActualValidator() { // Given: - initializer = ResourceInitializer.resourceInitializer(resourceCreator); + initializer = ResourceInitializer.resourceInitializer(callbacks); // Then: assertThrows(RuntimeException.class, () -> initializer.init(List.of(component0)));