diff --git a/spring-geode/src/main/java/org/springframework/geode/core/io/ResourceResolver.java b/spring-geode/src/main/java/org/springframework/geode/core/io/ResourceResolver.java new file mode 100644 index 00000000..667a28a0 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/core/io/ResourceResolver.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.geode.core.io; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.util.Optional; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; + +/** + * Interface defining a contract encapsulating an algorithm/strategy for resolving {@link Resource Resources}. + * + * @author John Blum + * @see org.springframework.core.io.Resource + * @since 1.3.1. + */ +@FunctionalInterface +public interface ResourceResolver { + + String CLASSPATH_URL_PREFIX = ResourceLoader.CLASSPATH_URL_PREFIX; + String FILESYSTEM_URL_PREFIX = "file:"; + String RESOURCE_PATH_SEPARATOR = "/"; + + /** + * Gets the {@link ClassLoader} used by this {@link ResourceResolver} to resolve {@literal classpath} + * {@link Resource Resources}. + * + * By default, this method will return a {@link ClassLoader} determined by {@link ClassUtils#getDefaultClassLoader()}, + * which first tries to return the {@link Thread#getContextClassLoader()}, then {@link Class#getClassLoader()}, + * and finally, {@link ClassLoader#getSystemClassLoader()}. + * + * @return an {@link Optional} {@link ClassLoader} used to resolve {@literal classpath} {@link Resource Resources}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + * @see java.lang.ClassLoader + * @see java.util.Optional + */ + default Optional getClassLoader() { + return Optional.ofNullable(ClassUtils.getDefaultClassLoader()); + } + + /** + * Tries to resolve a {@link Resource} handle from the given, {@literal non-null} {@link String location} + * (e.g. {@link String filesystem path}). + * + * @param location {@link String location} identifying the {@link Resource} to resolve; + * must not be {@literal null}. + * @return an {@link Optional} {@link Resource} handle for the given {@link String location}. + * @see org.springframework.core.io.Resource + * @see java.util.Optional + */ + Optional resolve(@NonNull String location); + + /** + * Returns a {@literal non-null}, {@literal existing} {@link Resource} handle resolved from the given, + * {@literal non-null} {@link String location} (e.g. {@link String filesystem path}). + * + * @param location {@link String location} identifying the {@link Resource} to resolve; + * must not be {@literal null}. + * @return a {@literal non-null}, {@literal existing} {@link Resource} handle for the resolved + * {@link String location}. + * @throws IllegalStateException if a {@link Resource} cannot be resolved from the given {@link String location}. + * A {@link Resource} is unresolvable if the given {@link String location} does not exist (physically); + * see {@link Resource#exists()}. + * @see org.springframework.core.io.Resource + * @see #resolve(String) + */ + default @NonNull Resource require(@NonNull String location) { + return resolve(location) + .filter(Resource::exists) + .orElseThrow(() -> newIllegalStateException("Resource [%s] does not exist", location)); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/core/io/ResourceResolverUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/core/io/ResourceResolverUnitTests.java new file mode 100644 index 00000000..191d720e --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/core/io/ResourceResolverUnitTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.geode.core.io; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Optional; +import java.util.Set; + +import org.junit.Test; + +import org.apache.shiro.util.CollectionUtils; + +import org.springframework.core.io.Resource; + +/** + * Unit Tests for {@link ResourceResolver}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.core.io.Resource + * @see org.springframework.geode.core.io.ResourceResolver + * @since 1.3.1 + */ +public class ResourceResolverUnitTests { + + @Test + public void getClassLoaderIsPresent() { + + ResourceResolver resourceResolver = mock(ResourceResolver.class); + + doCallRealMethod().when(resourceResolver).getClassLoader(); + + Set classLoaders = CollectionUtils.asSet( + Thread.currentThread().getContextClassLoader(), + getClass().getClassLoader(), + ClassLoader.getSystemClassLoader() + ); + + assertThat(classLoaders).contains(resourceResolver.getClassLoader().orElse(null)); + } + + @Test + public void requireCallsResolveReturningExistingResourceForLocation() { + + String location = "/path/to/resource.dat"; + + Resource mockResource = mock(Resource.class); + + ResourceResolver resourceResolver = mock(ResourceResolver.class); + + doReturn(true).when(mockResource).exists(); + doReturn(Optional.of(mockResource)).when(resourceResolver).resolve(eq(location)); + doCallRealMethod().when(resourceResolver).require(any()); + + assertThat(resourceResolver.require(location)).isEqualTo(mockResource); + + verify(resourceResolver, times(1)).resolve(eq(location)); + verify(mockResource, times(1)).exists(); + } + + @Test(expected = IllegalStateException.class) + public void requireCallsResolveReturningNonExistingResourceThrowsIllegalStateException() { + + String location = "/path/to/non-existing/resource.dat"; + + Resource mockResource = mock(Resource.class); + + ResourceResolver resourceResolver = mock(ResourceResolver.class); + + doReturn(false).when(mockResource).exists(); + doReturn(Optional.of(mockResource)).when(resourceResolver).resolve(eq(location)); + doCallRealMethod().when(resourceResolver).require(any()); + + try { + resourceResolver.require(location); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("Resource [%s] does not exist", location); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(resourceResolver, times(1)).resolve(eq(location)); + verify(mockResource, times(1)).exists(); + } + } + + @Test(expected = IllegalStateException.class) + public void requireCallsResolveReturningNoResourceThrowsIllegalStateException() { + + String location = "/path/to/nowhere"; + + ResourceResolver resourceResolver = mock(ResourceResolver.class); + + doReturn(Optional.empty()).when(resourceResolver).resolve(eq(location)); + doCallRealMethod().when(resourceResolver).require(any()); + + try { + resourceResolver.require(location); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("Resource [%s] does not exist", location); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verify(resourceResolver, times(1)).resolve(eq(location)); + } + } +}