diff --git a/core/citrus-api/src/main/java/org/citrusframework/spi/Resources.java b/core/citrus-api/src/main/java/org/citrusframework/spi/Resources.java index cebf5fec9d..5b174466e9 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/spi/Resources.java +++ b/core/citrus-api/src/main/java/org/citrusframework/spi/Resources.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -19,6 +19,9 @@ package org.citrusframework.spi; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.util.ReflectionHelper; + import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -33,8 +36,7 @@ import java.net.URLConnection; import java.nio.file.Paths; -import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.util.ReflectionHelper; +import static java.lang.String.format; /** * Helps with resources of type classpath or file system. @@ -47,6 +49,10 @@ public class Resources { public static final String JAR_RESOURCE_PREFIX = "jar:"; public static final String HTTP_RESOURCE_PREFIX = "http:"; + private Resources() { + // static access only + } + public static Resource create(String filePath) { if (filePath.startsWith(CLASSPATH_RESOURCE_PREFIX)) { return fromClasspath(filePath); @@ -87,6 +93,7 @@ public static Resource create(URL url) { public static Resource fromClasspath(String filePath) { return new ClasspathResource(filePath); } + public static Resource fromClasspath(String filePath, Class contextClass) { return fromClasspath(contextClass.getPackageName().replace(".", "/") + "/" + filePath); } @@ -96,7 +103,9 @@ public static Resource fromFileSystem(String filePath) { } /** - * Removes leading resource type information from given file path for classpath and file system typed resources. + * Removes leading resource type information from given file path for classpath and file system + * typed resources. + * * @param filePath * @return */ @@ -141,18 +150,22 @@ public boolean exists() { @Override public InputStream getInputStream() { - return ReflectionHelper.class.getClassLoader().getResourceAsStream(location.replace("\\","/")); + return ReflectionHelper.class.getClassLoader() + .getResourceAsStream(location.replace("\\", "/")); } @Override public File getFile() { if (!exists()) { - throw new CitrusRuntimeException(String.format("Failed to load classpath resource %s - does not exist", getLocation())); + throw new CitrusRuntimeException( + format("Failed to load classpath resource %s - does not exist", getLocation()) + ); } return Paths.get(getURI()).toFile(); } + @Override public URI getURI() { URL url = ReflectionHelper.class.getClassLoader().getResource(location); try { @@ -191,7 +204,8 @@ public InputStream getInputStream() { @Override public File getFile() { - throw new UnsupportedOperationException("ByteArrayResource does not provide access to a file"); + throw new UnsupportedOperationException( + "ByteArrayResource does not provide access to a file"); } } @@ -263,11 +277,25 @@ public String getLocation() { @Override public boolean exists() { + if (url == null) { + return false; + } + + if ("file".equals(url.getProtocol())) { + try { + // If we have a protocol, it means that the url is + // absolute and the file should be resolvable. + return new File(url.toURI()).exists(); + } catch (URISyntaxException e) { + throw new CitrusRuntimeException("Unable to parse absolute file url: " + url); + } + } + URLConnection connection = null; try { connection = url.openConnection(); - if (connection instanceof HttpURLConnection) { - return ((HttpURLConnection) connection).getResponseCode() == HttpURLConnection.HTTP_OK; + if (connection instanceof HttpURLConnection httpURLConnection) { + return httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK; } return connection.getContentLengthLong() > 0; @@ -276,8 +304,8 @@ public boolean exists() { } finally { // close the http connection to avoid // leaking gaps in case of an exception - if (connection instanceof HttpURLConnection) { - ((HttpURLConnection) connection).disconnect(); + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); } } } @@ -294,8 +322,8 @@ public InputStream getInputStream() { } finally { // close the http connection to avoid // leaking gaps in case of an exception - if (connection instanceof HttpURLConnection) { - ((HttpURLConnection) connection).disconnect(); + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); } } } @@ -303,7 +331,9 @@ public InputStream getInputStream() { @Override public File getFile() { if (!"file".equals(url.getProtocol())) { - throw new CitrusRuntimeException("Failed to resolve to absolute file path because it does not reside in the file system: " + url); + throw new CitrusRuntimeException( + format("Failed to resolve to absolute file path because it does not reside in the file system: %s", url) + ); } try { return new File(url.toURI().getSchemeSpecificPart()); diff --git a/core/citrus-api/src/test/java/org/citrusframework/spi/ResourcesTest.java b/core/citrus-api/src/test/java/org/citrusframework/spi/ResourcesTest.java new file mode 100644 index 0000000000..bf27253577 --- /dev/null +++ b/core/citrus-api/src/test/java/org/citrusframework/spi/ResourcesTest.java @@ -0,0 +1,219 @@ +package org.citrusframework.spi; + +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resources.ByteArrayResource; +import org.citrusframework.spi.Resources.ClasspathResource; +import org.citrusframework.spi.Resources.FileSystemResource; +import org.citrusframework.spi.Resources.UrlResource; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertThrows; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertNotNull; +import static org.testng.AssertJUnit.assertTrue; + +public class ResourcesTest { + + private static URI baseUri; + private static URI baseFolderUri; + private static URI fileWithContentUri; + private static URI fileWithoutContentUri; + private static URI nonExistingFileUri; + + @BeforeClass + public static void beforeClass() throws URISyntaxException { + Resource resource = Resources.create("/ResourcesTest"); + baseUri = resource.getFile().getParentFile().toURI(); + baseFolderUri = resource.getFile().toURI(); + + fileWithContentUri = new URI(baseFolderUri + "FileWithContent.json"); + fileWithoutContentUri = new URI(baseFolderUri + "FileWithoutContent.txt"); + nonExistingFileUri = new URI(baseFolderUri + "NonExistingFile.txt"); + } + + @Test + public void urlNullDoesNotExistTest() { + assertFalse(new UrlResource(null).exists()); + } + + @Test + public void classpathResourceTest() { + Resource withContentResource = Resources.create( + baseUri.relativize(fileWithContentUri).getPath()); + assertTrue(withContentResource.exists()); + + Resource withoutContentResource = Resources.create( + baseUri.relativize(fileWithoutContentUri).getPath()); + assertTrue(withoutContentResource.exists()); + + Resource nonExistingResource = Resources.create( + baseUri.relativize(nonExistingFileUri).getPath()); + assertFalse(nonExistingResource.exists()); + } + + @Test + public void byteArrayResourceTest() { + Resource byteArrayResource = new ByteArrayResource(new byte[100]); + assertEquals(byteArrayResource.getLocation(), ""); + assertTrue(byteArrayResource.exists()); + assertTrue(byteArrayResource.getInputStream() instanceof ByteArrayInputStream); + assertThrows(UnsupportedOperationException.class, byteArrayResource::getFile); + } + + @Test + public void defaultFileSystemResourceTest() { + Resource resource = Resources.create("/ResourcesTest"); + File resourceFolder = resource.getFile(); + + assertTrue(resourceFolder.exists()); + assertTrue(resourceFolder.isDirectory()); + + Resource withContentResource = Resources.create(fileWithContentUri.getPath()); + assertTrue(withContentResource instanceof FileSystemResource); + assertTrue(withContentResource.exists()); + + Resource withoutContentResource = Resources.create(fileWithoutContentUri.getPath()); + assertTrue(withoutContentResource instanceof FileSystemResource); + assertTrue(withoutContentResource.exists()); + + Resource nonExistingResource = Resources.create(nonExistingFileUri.getPath()); + assertTrue(nonExistingResource instanceof ClasspathResource); + assertFalse(nonExistingResource.exists()); + } + + @Test + public void fileSystemResourceTest() throws IOException { + Resource resource = Resources.create("/ResourcesTest"); + File file = resource.getFile(); + + assertTrue(file.exists()); + assertTrue(file.isDirectory()); + + Resource withContentResource = Resources.create(fileWithContentUri.toString()); + assertTrue(withContentResource instanceof FileSystemResource); + assertTrue(withContentResource.exists()); + assertEquals(fileWithContentUri, withContentResource.getURI()); + assertEquals(new File(fileWithContentUri).getPath(), withContentResource.getLocation()); + try (InputStream inputStream = withContentResource.getInputStream()) { + assertNotNull(inputStream); + } + + Resource withoutContentResource = Resources.create(fileWithoutContentUri.toString()); + assertTrue(withoutContentResource instanceof FileSystemResource); + assertTrue(withoutContentResource.exists()); + try (InputStream inputStream = withoutContentResource.getInputStream()) { + assertNotNull(inputStream); + } + + Resource nonExistingResource = Resources.create(nonExistingFileUri.toString()); + assertTrue(nonExistingResource instanceof FileSystemResource); + assertFalse(nonExistingResource.exists()); + assertThrows(CitrusRuntimeException.class, nonExistingResource::getInputStream); + + assertThrows(UnsupportedOperationException.class, () -> new FileSystemResource(new File(baseFolderUri)).getInputStream()); + } + + @Test + public void urlResourceTest() throws MalformedURLException { + Resource httpResource = Resources.create(Resources.HTTP_RESOURCE_PREFIX + "//host/context"); + assertTrue(httpResource instanceof UrlResource); + + URL withContentUrlMock = spy(fileWithContentUri.toURL()); + UrlResource withContentResource = new UrlResource(withContentUrlMock); + assertTrue(withContentResource.exists()); + assertEquals(new File(fileWithContentUri), withContentResource.getFile()); + assertEquals(fileWithContentUri.toURL().toString(), withContentResource.getLocation()); + + URL withoutContentUrlMock = spy(fileWithoutContentUri.toURL()); + UrlResource withoutContentResource = new UrlResource(withoutContentUrlMock); + assertTrue(withoutContentResource.exists()); + assertEquals(new File(fileWithoutContentUri), withoutContentResource.getFile()); + + URL nonExistingUrlMock = spy(nonExistingFileUri.toURL()); + UrlResource nonExistingResource = new UrlResource(nonExistingUrlMock); + assertFalse(nonExistingResource.exists()); + assertEquals(new File(nonExistingFileUri), nonExistingResource.getFile()); + } + + @Test + public void urlResourceExistsOnConnectionTest() throws IOException { + URL withContentUrlMock = spy(new URL(Resources.HTTP_RESOURCE_PREFIX + "//host/context")); + HttpURLConnection urlConnectionMock = mock(HttpURLConnection.class); + doReturn(urlConnectionMock).when(withContentUrlMock).openConnection(); + doReturn(HttpURLConnection.HTTP_OK).when(urlConnectionMock).getResponseCode(); + assertTrue(new UrlResource(withContentUrlMock).exists()); + } + + + @Test + public void urlResourceNotExistsOnConnectionFailureTest() throws IOException { + URL withContentUrlMock = spy(new URL(Resources.HTTP_RESOURCE_PREFIX + "//host/context")); + HttpURLConnection urlConnectionMock = mock(HttpURLConnection.class); + doReturn(urlConnectionMock).when(withContentUrlMock).openConnection(); + doReturn(HttpURLConnection.HTTP_INTERNAL_ERROR).when(urlConnectionMock).getResponseCode(); + assertFalse(new UrlResource(withContentUrlMock).exists()); + } + + @Test + public void uriSyntaxThrowsCitrusRuntimeException() + throws MalformedURLException, URISyntaxException { + URL withContentUrlMock = spy(fileWithContentUri.toURL()); + doThrow(new URISyntaxException("xxxx", "Test Exception[uriSyntaxThrowsCitrusRuntimeException]")) + .when(withContentUrlMock).toURI(); + UrlResource withContentResource = new UrlResource(withContentUrlMock); + assertThrows(CitrusRuntimeException.class, withContentResource::exists); + } + + @Test + public void urlResourceNotExistsOnIOExceptionTest() throws IOException { + URL withContentUrlMock = spy(new URL(Resources.HTTP_RESOURCE_PREFIX + "//host/context")); + doThrow(new IOException("Test Exception[urlResourceNotExistsOnIoExceptionTest]")) + .when(withContentUrlMock).openConnection(); + assertThrows(CitrusRuntimeException.class, () -> new UrlResource(withContentUrlMock).exists()); + } + + @Test + public void urlResourceInputStreamTest() throws IOException { + URL withContentUrlMock = spy(new URL(Resources.HTTP_RESOURCE_PREFIX + "//host/context")); + HttpURLConnection urlConnectionMock = mock(HttpURLConnection.class); + InputStream inputStreamMock = mock(InputStream.class); + + doReturn(urlConnectionMock).when(withContentUrlMock).openConnection(); + doReturn(inputStreamMock).when(urlConnectionMock).getInputStream(); + assertEquals(inputStreamMock, new UrlResource(withContentUrlMock).getInputStream()); + + verify(urlConnectionMock).disconnect(); + } + + @Test + public void urlResourceInputStreamIOExceptionTest() throws IOException { + URL withContentUrlMock = spy(new URL(Resources.HTTP_RESOURCE_PREFIX + "//host/context")); + HttpURLConnection urlConnectionMock = mock(HttpURLConnection.class); + + doReturn(urlConnectionMock).when(withContentUrlMock).openConnection(); + + doThrow(new IOException("Test Exception[urlResourceInputStreamIOExceptionTest]")) + .when(urlConnectionMock).getInputStream(); + + assertThrows(CitrusRuntimeException.class, () -> new UrlResource(withContentUrlMock).getInputStream()); + + verify(urlConnectionMock).disconnect(); + } +} diff --git a/core/citrus-api/src/test/resources/ResourcesTest/FileWithContent.json b/core/citrus-api/src/test/resources/ResourcesTest/FileWithContent.json new file mode 100644 index 0000000000..e00a19c7c3 --- /dev/null +++ b/core/citrus-api/src/test/resources/ResourcesTest/FileWithContent.json @@ -0,0 +1 @@ +{"name": "Max Mustermann"} \ No newline at end of file diff --git a/core/citrus-api/src/test/resources/ResourcesTest/FileWithoutContent.txt b/core/citrus-api/src/test/resources/ResourcesTest/FileWithoutContent.txt new file mode 100644 index 0000000000..e69de29bb2