diff --git a/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java b/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java new file mode 100644 index 000000000000..6ba2083cb221 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2023 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.core.io; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for {@link java.lang.Module} resolution, + * performing {@link #getInputStream()} access via {@link Module#getResourceAsStream}. + * + *

Alternatively, consider accessing resources in a module path layout + * via {@link ClassPathResource} for exported resources, or specifically + * via {@link ClassPathResource#ClassPathResource(String, Class)} + * for local resolution within the containing module of a specific class. + * + * @author Juergen Hoeller + * @since 6.1 + * @see Module#getResourceAsStream + * @see ClassPathResource + */ +public class ModuleResource extends AbstractResource { + + private final Module module; + + private final String path; + + + /** + * Create a new {@code ModuleResource} for the given {@link Module} + * and the given resource path. + * @param module the runtime module to search within + * @param path the resource path within the module + */ + public ModuleResource(Module module, String path) { + Assert.notNull(module, "Module must not be null"); + Assert.notNull(path, "Path must not be null"); + this.module = module; + this.path = path; + } + + + /** + * Return the {@link Module} for this resource. + */ + public final Module getModule() { + return this.module; + } + + /** + * Return the path for this resource. + */ + public final String getPath() { + return this.path; + } + + + @Override + public InputStream getInputStream() throws IOException { + InputStream is = this.module.getResourceAsStream(this.path); + if (is == null) { + throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); + } + return is; + } + + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new ModuleResource(this.module, pathToUse); + } + + @Override + public String getDescription() { + return "module resource [" + this.path + "]" + + (this.module.isNamed() ? " from module '" + this.module.getName() + "'" : ""); + } + + + @Override + public boolean equals(@Nullable Object obj) { + return (this == obj || (obj instanceof ModuleResource that && + this.module.equals(that.module) && this.path.equals(that.path))); + } + + @Override + public int hashCode() { + return this.module.hashCode() * 31 + this.path.hashCode(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/ModuleResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ModuleResourceTests.java new file mode 100644 index 000000000000..150af1f1c223 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/ModuleResourceTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2023 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.core.io; + +import java.beans.Introspector; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for the {@link ModuleResource} class. + * + * @author Juergen Hoeller + * @since 6.1 + */ +class ModuleResourceTests { + + @Test + void existingResource() throws IOException { + ModuleResource mr = new ModuleResource(Introspector.class.getModule(), "java/beans/Introspector.class"); + assertThat(mr.exists()).isTrue(); + assertThat(mr.isReadable()).isTrue(); + assertThat(mr.isOpen()).isFalse(); + assertThat(mr.isFile()).isFalse(); + assertThat(mr.getDescription()).contains(mr.getModule().getName()); + assertThat(mr.getDescription()).contains(mr.getPath()); + + Resource cpr = new ClassPathResource("java/beans/Introspector.class"); + assertThat(mr.getContentAsByteArray()).isEqualTo(cpr.getContentAsByteArray()); + assertThat(mr.contentLength()).isEqualTo(cpr.contentLength()); + } + + @Test + void nonExistingResource() { + ModuleResource mr = new ModuleResource(Introspector.class.getModule(), "java/beans/Introspecter.class"); + assertThat(mr.exists()).isFalse(); + assertThat(mr.isReadable()).isFalse(); + assertThat(mr.isOpen()).isFalse(); + assertThat(mr.isFile()).isFalse(); + assertThat(mr.getDescription()).contains(mr.getModule().getName()); + assertThat(mr.getDescription()).contains(mr.getPath()); + + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(mr::getContentAsByteArray); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(mr::contentLength); + } + + @Test + void equalsAndHashCode() { + Resource mr1 = new ModuleResource(Introspector.class.getModule(), "java/beans/Introspector.class"); + Resource mr2 = new ModuleResource(Introspector.class.getModule(), "java/beans/Introspector.class"); + Resource mr3 = new ModuleResource(Introspector.class.getModule(), "java/beans/Introspecter.class"); + assertThat(mr1).isEqualTo(mr2); + assertThat(mr1).isNotEqualTo(mr3); + assertThat(mr1).hasSameHashCodeAs(mr2); + assertThat(mr1).doesNotHaveSameHashCodeAs(mr3); + } + +}