From 26fa7d4e0d45eaddbae852ecc5ec1c269e4b93e5 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Thu, 14 May 2026 13:33:03 -0400 Subject: [PATCH 1/4] Improve compatibility with newer Java and Windows ZIP handling - adapt Spring integration and related tests to stricter null-safety and bean lookup handling - update library integrations and tests for current APIs, deprecations, and warnings - fix temp-file permission handling on non-POSIX filesystems and tighten ZIP stream cleanup Refs #1505 --- .../ext/freemarker/FreeMarkerTestCase.java | 3 +- .../restlet/ext/gson/GsonRepresentation.java | 2 +- .../java/org/restlet/ext/jaas/JaasUtils.java | 1 + .../ext/jackson/JacksonRepresentation.java | 2 +- .../restlet/ext/jackson/JacksonTestCase.java | 7 +- .../src/main/java/org/restlet/JSON.gwt.xml | 4 - .../restlet/ext/json/JsonRepresentation.java | 2 +- .../internal/RestletOpenApiContext.java | 4 +- .../restlet/ext/openapi/LibraryExample.java | 1 - .../restlet/ext/spring/SpringBeanFinder.java | 30 +++++--- .../restlet/ext/spring/SpringBeanRouter.java | 74 +++++++++++++++--- .../org/restlet/ext/spring/SpringContext.java | 1 + .../restlet/ext/spring/SpringResource.java | 16 +++- .../ext/spring/SpringBeanFinderTestCase.java | 75 +++++++++++++++---- .../ext/spring/SpringBeanRouterTestCase.java | 38 ++++++++-- .../ext/thymeleaf/TemplateRepresentation.java | 5 -- .../ext/xml/ResolvingTransformerTestCase.java | 8 +- .../src/main/java/org/restlet/Response.java | 1 - .../src/main/java/org/restlet/Restlet.java | 1 + .../restlet/engine/adapter/ServerCall.java | 3 + .../engine/converter/DefaultConverter.java | 1 + .../engine/local/FileClientHelper.java | 20 +++-- .../restlet/engine/local/ZipClientHelper.java | 17 +++-- .../engine/local/ZipEntryRepresentation.java | 16 +++- .../resource/ClientInvocationHandler.java | 7 +- .../engine/resource/MethodAnnotationInfo.java | 8 +- .../org/restlet/resource/ClientResource.java | 1 + .../java/org/restlet/util/WrapperList.java | 2 +- .../MultiPartRepresentationTestCase.java | 8 +- .../connector/ShutdownHookTestCase.java | 2 +- .../connector/SslBaseConnectorsTestCase.java | 1 - .../resource/AnnotatedResource13TestCase.java | 4 +- .../service/StatusServiceTestCase.java | 4 - 33 files changed, 271 insertions(+), 98 deletions(-) delete mode 100644 org.restlet.ext.json/src/main/java/org/restlet/JSON.gwt.xml diff --git a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java index 7afafb2b70..3c0c27a260 100644 --- a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java +++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java @@ -8,6 +8,7 @@ */ package org.restlet.ext.freemarker; +import static freemarker.template.Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS; import static org.junit.jupiter.api.Assertions.assertEquals; import freemarker.template.Configuration; @@ -37,7 +38,7 @@ void testTemplate() throws Exception { fw.write("Value=${value}"); fw.close(); - final Configuration fmc = new Configuration(); + final Configuration fmc = new Configuration(DEFAULT_INCOMPATIBLE_IMPROVEMENTS); fmc.setDirectoryForTemplateLoading(testDir); final Map map = Map.of("value", "myValue"); diff --git a/org.restlet.ext.gson/src/main/java/org/restlet/ext/gson/GsonRepresentation.java b/org.restlet.ext.gson/src/main/java/org/restlet/ext/gson/GsonRepresentation.java index 9ba5dae9d3..bc6fa143ed 100644 --- a/org.restlet.ext.gson/src/main/java/org/restlet/ext/gson/GsonRepresentation.java +++ b/org.restlet.ext.gson/src/main/java/org/restlet/ext/gson/GsonRepresentation.java @@ -110,7 +110,7 @@ public GsonRepresentation(T object) { */ protected GsonBuilder createBuilder() { GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setDateFormat(DateFormat.FULL); + gsonBuilder.setDateFormat(DateFormat.FULL, DateFormat.FULL); return gsonBuilder; } diff --git a/org.restlet.ext.jaas/src/main/java/org/restlet/ext/jaas/JaasUtils.java b/org.restlet.ext.jaas/src/main/java/org/restlet/ext/jaas/JaasUtils.java index fe0daa4dcf..393c6c477d 100644 --- a/org.restlet.ext.jaas/src/main/java/org/restlet/ext/jaas/JaasUtils.java +++ b/org.restlet.ext.jaas/src/main/java/org/restlet/ext/jaas/JaasUtils.java @@ -74,6 +74,7 @@ public static T doAsPriviledged(ClientInfo clientInfo, PrivilegedAction a * @param acc the AccessControlContext to be tied to the specified subject and action. * @return the value returned by the action. */ + @SuppressWarnings("removal") public static T doAsPriviledged( ClientInfo clientInfo, PrivilegedAction action, AccessControlContext acc) { Subject subject = JaasUtils.createSubject(clientInfo); diff --git a/org.restlet.ext.jackson/src/main/java/org/restlet/ext/jackson/JacksonRepresentation.java b/org.restlet.ext.jackson/src/main/java/org/restlet/ext/jackson/JacksonRepresentation.java index be94e8cc70..4d00816d3d 100644 --- a/org.restlet.ext.jackson/src/main/java/org/restlet/ext/jackson/JacksonRepresentation.java +++ b/org.restlet.ext.jackson/src/main/java/org/restlet/ext/jackson/JacksonRepresentation.java @@ -232,7 +232,7 @@ protected ObjectWriter createObjectWriter() { CsvSchema csvSchema = createCsvSchema(csvMapper); result = csvMapper.writer(csvSchema); } else { - result = getObjectMapper().writerWithType(getObjectClass()); + result = getObjectMapper().writerFor(getObjectClass()); } return result; diff --git a/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java b/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java index a66b26ed85..ed460f58a0 100644 --- a/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java +++ b/org.restlet.ext.jackson/src/test/java/org/restlet/ext/jackson/JacksonTestCase.java @@ -147,7 +147,8 @@ void testXmlBomb() { Undeclared general entity "lol10" at [row,col {unknown-source}]: [14,31] at [Source: (BufferedInputStream); line: 14, column: 32]"""; - Assertions.assertEquals(expected, exception.getMessage()); + Assertions.assertEquals( + normalizeLineEndings(expected), normalizeLineEndings(exception.getMessage())); } @Test @@ -203,4 +204,8 @@ protected void assertEquals(MyException me1, MyException me2) { Assertions.assertEquals(me1.getErrorCode(), me2.getErrorCode()); assertEquals(me1.getCustomer(), me2.getCustomer()); } + + private String normalizeLineEndings(String text) { + return text.replace("\r\n", "\n").replace('\r', '\n'); + } } diff --git a/org.restlet.ext.json/src/main/java/org/restlet/JSON.gwt.xml b/org.restlet.ext.json/src/main/java/org/restlet/JSON.gwt.xml deleted file mode 100644 index 5497a3ccb7..0000000000 --- a/org.restlet.ext.json/src/main/java/org/restlet/JSON.gwt.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/org.restlet.ext.json/src/main/java/org/restlet/ext/json/JsonRepresentation.java b/org.restlet.ext.json/src/main/java/org/restlet/ext/json/JsonRepresentation.java index 0bd254fef9..682b2de57a 100644 --- a/org.restlet.ext.json/src/main/java/org/restlet/ext/json/JsonRepresentation.java +++ b/org.restlet.ext.json/src/main/java/org/restlet/ext/json/JsonRepresentation.java @@ -100,7 +100,7 @@ public JsonRepresentation(Map map) { * @see org.json.JSONObject#JSONObject(Object) */ public JsonRepresentation(Object bean) { - this(new JSONObject(bean)); // TODO Should be called if Android edition + this(new JSONObject(bean)); } /** diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java index 8391f48230..b18e985c81 100644 --- a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java +++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java @@ -10,13 +10,11 @@ import io.swagger.v3.oas.integration.GenericOpenApiContext; import io.swagger.v3.oas.integration.api.OpenAPIConfiguration; -import io.swagger.v3.oas.integration.api.OpenApiContext; import io.swagger.v3.oas.integration.api.OpenApiReader; import org.apache.commons.lang3.StringUtils; import org.restlet.routing.Router; -public class RestletOpenApiContext extends GenericOpenApiContext - implements OpenApiContext { +public class RestletOpenApiContext extends GenericOpenApiContext { private final Router router; public RestletOpenApiContext(Router router) { diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java index c852ba075b..0657b8c09a 100644 --- a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java @@ -24,7 +24,6 @@ import org.restlet.resource.ServerResource; import org.restlet.routing.Router; -@SuppressWarnings("unused") public class LibraryExample { private static final List BOOKS = List.of( diff --git a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanFinder.java b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanFinder.java index 7e542fb48f..9ef4bcb0d8 100644 --- a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanFinder.java +++ b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanFinder.java @@ -15,6 +15,7 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; /** * An alternative to {@link SpringFinder} which uses Spring's BeanFactory mechanism to load a @@ -54,7 +55,7 @@ public SpringBeanFinder() {} * @param beanFactory The Spring bean factory. * @param beanName The bean name. */ - public SpringBeanFinder(Router router, BeanFactory beanFactory, String beanName) { + public SpringBeanFinder(Router router, @NonNull BeanFactory beanFactory, String beanName) { this.router = router; setBeanFactory(beanFactory); setBeanName(beanName); @@ -63,10 +64,11 @@ public SpringBeanFinder(Router router, BeanFactory beanFactory, String beanName) @Override public ServerResource create() { final Object resource = findBean(); + String requiredBeanName = getRequiredBeanName(); if (!(resource instanceof ServerResource)) { throw new ClassCastException( - getBeanName() + requiredBeanName + " does not resolve to an instance of " + org.restlet.resource.ServerResource.class.getName()); } @@ -75,20 +77,30 @@ public ServerResource create() { } private Object findBean() { + String requiredBeanName = getRequiredBeanName(); if (getBeanFactory() == null && getApplicationContext() == null) { throw new IllegalStateException( "Either a beanFactory or an applicationContext is required for SpringBeanFinder."); } else if (getApplicationContext() != null - && getApplicationContext().containsBean(getBeanName())) { - return getApplicationContext().getBean(getBeanName()); - } else if (getBeanFactory() != null && getBeanFactory().containsBean(getBeanName())) { - return getBeanFactory().getBean(getBeanName()); + && getApplicationContext().containsBean(requiredBeanName)) { + return getApplicationContext().getBean(requiredBeanName); + } else if (getBeanFactory() != null && getBeanFactory().containsBean(requiredBeanName)) { + return getBeanFactory().getBean(requiredBeanName); } else { throw new IllegalStateException( - String.format("No bean named %s present.", getBeanName())); + String.format("No bean named %s present.", requiredBeanName)); } } + @NonNull + private String getRequiredBeanName() { + String currentBeanName = getBeanName(); + if (currentBeanName == null) { + throw new IllegalStateException("beanName"); + } + return currentBeanName; + } + /** * Returns the parent application context. * @@ -135,7 +147,7 @@ public Router getRouter() { * * @param applicationContext The parent context. */ - public void setApplicationContext(ApplicationContext applicationContext) { + public void setApplicationContext(@NonNull ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @@ -144,7 +156,7 @@ public void setApplicationContext(ApplicationContext applicationContext) { * * @param beanFactory The parent bean factory. */ - public void setBeanFactory(BeanFactory beanFactory) { + public void setBeanFactory(@NonNull BeanFactory beanFactory) { this.beanFactory = beanFactory; } diff --git a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanRouter.java b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanRouter.java index f66f988f91..33651eafca 100644 --- a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanRouter.java +++ b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringBeanRouter.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.NonNull; /** * Restlet {@link Router} which behaves like Spring's {@link @@ -136,7 +137,7 @@ protected void attachResource(String uri, String beanName, BeanFactory beanFacto * @param beanFactory The Spring bean factory. */ protected void attachRestlet(String uri, String beanName, BeanFactory beanFactory) { - attach(uri, (Restlet) beanFactory.getBean(beanName)); + attach(uri, (Restlet) requiredBeanFactory(beanFactory).getBean(requiredBeanName(beanName))); } /** @@ -147,7 +148,8 @@ protected void attachRestlet(String uri, String beanName, BeanFactory beanFactor * @see #attachResource */ protected Finder createFinder(BeanFactory beanFactory, String beanName) { - return new SpringBeanFinder(this, beanFactory, beanName); + return new SpringBeanFinder( + this, requiredBeanFactory(beanFactory), requiredBeanName(beanName)); } /** @@ -169,8 +171,12 @@ protected Map getAttachments() { private String[] getBeanNamesByType(Class beanClass, ListableBeanFactory beanFactory) { return isFindingInAncestors() ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors( - beanFactory, beanClass, true, true) - : beanFactory.getBeanNamesForType(beanClass, true, true); + requiredListableBeanFactory(beanFactory), + requiredBeanClass(beanClass), + true, + true) + : requiredListableBeanFactory(beanFactory) + .getBeanNamesForType(requiredBeanClass(beanClass), true, true); } /** @@ -202,19 +208,19 @@ public boolean isFindingInAncestors() { * @param beanFactory The Spring bean factory. * @see #setAttachments */ - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + public void postProcessBeanFactory(@NonNull ConfigurableListableBeanFactory beanFactory) throws BeansException { ListableBeanFactory source = - this.applicationContext == null ? beanFactory : this.applicationContext; + this.applicationContext == null ? beanFactory : requiredApplicationContext(); attachAllResources(source); attachAllRestlets(source); if (getAttachments() != null) { for (Map.Entry attachment : getAttachments().entrySet()) { String uri = attachment.getKey(); - String beanName = attachment.getValue(); - Class beanType = source.getType(beanName); + String beanName = requiredBeanName(attachment.getValue()); + Class beanType = requiredBeanClass(source.getType(beanName)); if (org.restlet.resource.ServerResource.class.isAssignableFrom(beanType)) { attachResource(uri, beanName, source); @@ -239,11 +245,13 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) * @return The alias URI. */ protected String resolveUri(String beanName, ListableBeanFactory beanFactory) { - if (isAvailableUri(beanName)) { - return beanName; + String requiredBeanName = requiredBeanName(beanName); + if (isAvailableUri(requiredBeanName)) { + return requiredBeanName; } - for (final String alias : beanFactory.getAliases(beanName)) { + for (final String alias : + requiredListableBeanFactory(beanFactory).getAliases(requiredBeanName)) { if (isAvailableUri(alias)) { return alias; } @@ -257,10 +265,52 @@ protected String resolveUri(String beanName, ListableBeanFactory beanFactory) { * * @param applicationContext The context. */ - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + public void setApplicationContext(@NonNull ApplicationContext applicationContext) + throws BeansException { this.applicationContext = applicationContext; } + @NonNull + private String requiredBeanName(String beanName) { + if (beanName == null) { + throw new IllegalStateException("beanName"); + } + return beanName; + } + + @NonNull + private BeanFactory requiredBeanFactory(BeanFactory beanFactory) { + if (beanFactory == null) { + throw new IllegalStateException("beanFactory"); + } + return beanFactory; + } + + @NonNull + private ListableBeanFactory requiredListableBeanFactory(ListableBeanFactory beanFactory) { + if (beanFactory == null) { + throw new IllegalStateException("beanFactory"); + } + return beanFactory; + } + + @NonNull + private Class requiredBeanClass(Class beanClass) { + if (beanClass == null) { + throw new IllegalStateException("beanClass"); + } + return beanClass; + } + + @NonNull + private ApplicationContext requiredApplicationContext() { + ApplicationContext currentApplicationContext = this.applicationContext; + if (currentApplicationContext == null) { + throw new IllegalStateException("applicationContext"); + } + return currentApplicationContext; + } + /** * Sets an explicit mapping of URI templates to bean IDs to use in addition to the usual bean * name mapping behavior. If a URI template appears in both this mapping and as a bean name, the diff --git a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringContext.java b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringContext.java index 3dd4a3abf9..a0489924cd 100644 --- a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringContext.java +++ b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringContext.java @@ -111,6 +111,7 @@ public List getXmlConfigRefs() { } @Override + @SuppressWarnings("deprecation") public void refresh() { // If this context hasn't been loaded yet, read all the configurations // registered diff --git a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringResource.java b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringResource.java index 82d41f3821..19598f2019 100644 --- a/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringResource.java +++ b/org.restlet.ext.spring/src/main/java/org/restlet/ext/spring/SpringResource.java @@ -14,6 +14,8 @@ import org.restlet.engine.util.SystemUtils; import org.restlet.representation.Representation; import org.springframework.core.io.AbstractResource; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; /** * Spring Resource based on a Restlet Representation. DON'T GET CONFUSED, Spring's notion of @@ -23,7 +25,7 @@ */ public class SpringResource extends AbstractResource { /** The description. */ - private final String description; + @NonNull private final String description; /** Indicates if the representation has already been read. */ private volatile boolean read = false; @@ -46,7 +48,7 @@ public SpringResource(Representation representation) { * @param representation The description. * @param description The description. */ - public SpringResource(Representation representation, String description) { + public SpringResource(Representation representation, @Nullable String description) { if (representation == null) { throw new IllegalArgumentException("Representation must not be null"); } @@ -57,7 +59,7 @@ public SpringResource(Representation representation, String description) { /** {@inheritDoc} */ @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (obj == this) { return true; } @@ -79,6 +81,7 @@ public boolean exists() { * @return The description. */ @Override + @NonNull public String getDescription() { return this.description; } @@ -88,6 +91,7 @@ public String getDescription() { * multiple times. */ @Override + @NonNull public InputStream getInputStream() throws IOException, IllegalStateException { if (this.read && this.representation.isTransient()) { throw new IllegalStateException( @@ -95,7 +99,11 @@ public InputStream getInputStream() throws IOException, IllegalStateException { } this.read = true; - return this.representation.getStream(); + InputStream stream = this.representation.getStream(); + if (stream == null) { + throw new IllegalStateException("representation stream"); + } + return stream; } /** This implementation returns the hash code of the underlying InputStream. */ diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanFinderTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanFinderTestCase.java index c0b74a5b1e..3cd4b2f5bb 100644 --- a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanFinderTestCase.java +++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanFinderTestCase.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.lang.NonNull; /** * @author Rhett Sutphin @@ -64,8 +65,10 @@ private MutablePropertyValues createServerResourcePropertyValues() { private void registerApplicationContextBean( String beanName, Class resourceClass) { - this.applicationContext.registerPrototype(beanName, resourceClass); - this.applicationContext.refresh(); + requiredApplicationContext() + .registerPrototype( + requiredBeanName(beanName), requiredResourceClass(resourceClass)); + requiredApplicationContext().refresh(); } private void registerBeanFactoryBean(String beanName, Class resourceClass) { @@ -74,9 +77,13 @@ private void registerBeanFactoryBean(String beanName, Class resourceClass) { private void registerBeanFactoryBean( String beanName, Class resourceClass, MutablePropertyValues values) { - this.beanFactory.registerBeanDefinition( - beanName, - new RootBeanDefinition(resourceClass, new ConstructorArgumentValues(), values)); + requiredBeanFactory() + .registerBeanDefinition( + requiredBeanName(beanName), + new RootBeanDefinition( + requiredAnyClass(resourceClass), + new ConstructorArgumentValues(), + values)); } @BeforeEach @@ -105,7 +112,7 @@ void testBeanResolutionFailsWithNeitherApplicationContextOrBeanFactory() { @Test void testBeanResolutionFailsWhenNoMatchingBeanButThereIsABeanFactory() { - this.finder.setBeanFactory(beanFactory); + this.finder.setBeanFactory(requiredBeanFactory()); IllegalStateException iae = assertThrows(IllegalStateException.class, () -> this.finder.create()); @@ -114,7 +121,7 @@ void testBeanResolutionFailsWhenNoMatchingBeanButThereIsABeanFactory() { @Test void testBeanResolutionFailsWhenNoMatchingBeanButThereIsAnApplicationContext() { - this.finder.setApplicationContext(applicationContext); + this.finder.setApplicationContext(requiredApplicationContext()); IllegalStateException iae = assertThrows(IllegalStateException.class, () -> this.finder.create()); assertEquals("No bean named " + BEAN_NAME + " present.", iae.getMessage()); @@ -124,7 +131,7 @@ void testBeanResolutionFailsWhenNoMatchingBeanButThereIsAnApplicationContext() { void testExceptionWhenResourceBeanIsWrongType() { registerBeanFactoryBean(BEAN_NAME, String.class); - this.finder.setBeanFactory(beanFactory); + this.finder.setBeanFactory(requiredBeanFactory()); ClassCastException classCastException = assertThrows(ClassCastException.class, () -> this.finder.create()); @@ -138,7 +145,7 @@ void testPrefersApplicationContextOverBeanFactoryIfTheBeanIsInBoth() { registerApplicationContextBean(BEAN_NAME, SomeResource.class); registerBeanFactoryBean(BEAN_NAME, AnotherResource.class); - this.finder.setApplicationContext(applicationContext); + this.finder.setApplicationContext(requiredApplicationContext()); ServerResource actual = this.finder.create(); @@ -152,7 +159,7 @@ void testPrefersApplicationContextOverBeanFactoryIfTheBeanIsInBoth() { void testReturnsResourceBeanWhenExists() { registerBeanFactoryBean(BEAN_NAME, SomeResource.class); - this.finder.setBeanFactory(beanFactory); + this.finder.setBeanFactory(requiredBeanFactory()); final ServerResource actual = this.finder.create(); @@ -164,7 +171,7 @@ void testReturnsServerResourceBeanForLongFormOfCreate() { registerBeanFactoryBean( BEAN_NAME, SomeServerResource.class, createServerResourcePropertyValues()); - this.finder.setBeanFactory(beanFactory); + this.finder.setBeanFactory(requiredBeanFactory()); final ServerResource actual = this.finder.create(SomeServerResource.class, null, null); @@ -180,7 +187,7 @@ void testReturnsServerResourceBeanWhenExists() { registerBeanFactoryBean( BEAN_NAME, SomeServerResource.class, createServerResourcePropertyValues()); - this.finder.setBeanFactory(beanFactory); + this.finder.setBeanFactory(requiredBeanFactory()); final ServerResource actual = this.finder.create(); @@ -191,10 +198,52 @@ void testReturnsServerResourceBeanWhenExists() { void testUsesApplicationContextIfPresent() { registerApplicationContextBean(BEAN_NAME, SomeResource.class); - this.finder.setApplicationContext(applicationContext); + this.finder.setApplicationContext(requiredApplicationContext()); ServerResource actual = this.finder.create(); assertInstanceOf(SomeResource.class, actual, "Resource not the correct type"); } + + @NonNull + private StaticApplicationContext requiredApplicationContext() { + StaticApplicationContext currentApplicationContext = this.applicationContext; + if (currentApplicationContext == null) { + throw new IllegalStateException("applicationContext"); + } + return currentApplicationContext; + } + + @NonNull + private DefaultListableBeanFactory requiredBeanFactory() { + DefaultListableBeanFactory currentBeanFactory = this.beanFactory; + if (currentBeanFactory == null) { + throw new IllegalStateException("beanFactory"); + } + return currentBeanFactory; + } + + @NonNull + private String requiredBeanName(String beanName) { + if (beanName == null) { + throw new IllegalStateException("beanName"); + } + return beanName; + } + + @NonNull + private Class requiredResourceClass(Class resourceClass) { + if (resourceClass == null) { + throw new IllegalStateException("resourceClass"); + } + return resourceClass; + } + + @NonNull + private Class requiredAnyClass(Class resourceClass) { + if (resourceClass == null) { + throw new IllegalStateException("resourceClass"); + } + return resourceClass; + } } diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java index ee0ce9bc4c..56e54b8317 100644 --- a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java +++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.NonNull; /** * @author Rhett Sutphin @@ -84,7 +85,7 @@ private void assertFinderForBean(String expectedBeanName, Restlet restlet) { } private void doPostProcess() { - this.router.postProcessBeanFactory(this.factory); + this.router.postProcessBeanFactory(requiredBeanFactory()); } private TemplateRoute matchRouteFor(String uri) { @@ -105,10 +106,10 @@ public String resolve(String name) { private void registerBeanDefinition(String id, String alias, Class beanClass, String scope) { BeanDefinition bd = new RootBeanDefinition(beanClass); bd.setScope(scope == null ? BeanDefinition.SCOPE_SINGLETON : scope); - this.factory.registerBeanDefinition(id, bd); + requiredBeanFactory().registerBeanDefinition(requiredString(id), bd); if (alias != null) { - this.factory.registerAlias(id, alias); + requiredBeanFactory().registerAlias(requiredString(id), requiredString(alias)); } } @@ -146,7 +147,8 @@ void tearDownEach() { @Test void testExplicitAttachmentsMayBeRestlets() { String expected = "/protected/timber"; - this.router.setAttachments(Collections.singletonMap(expected, "timber")); + this.router.setAttachments( + Collections.singletonMap(requiredString(expected), requiredString("timber"))); registerBeanDefinition("timber", null, TestAuthenticator.class, null); doPostProcess(); @@ -158,7 +160,8 @@ void testExplicitAttachmentsMayBeRestlets() { @Test void testExplicitAttachmentsTrumpBeanNames() { - this.router.setAttachments(Collections.singletonMap(ORE_URI, "fish")); + this.router.setAttachments( + Collections.singletonMap(requiredString(ORE_URI), requiredString("fish"))); RouteList actualRoutes = actualRoutes(); assertEquals(2, actualRoutes.size(), "Wrong number of routes"); @@ -169,7 +172,9 @@ void testExplicitAttachmentsTrumpBeanNames() { @Test void testExplicitRoutingForNonResourceNonRestletBeansFails() { - this.router.setAttachments(Collections.singletonMap("/fail", "someOtherBean")); + this.router.setAttachments( + Collections.singletonMap( + requiredString("/fail"), requiredString("someOtherBean"))); IllegalStateException ise = assertThrows(IllegalStateException.class, this::doPostProcess); assertEquals( @@ -264,7 +269,9 @@ void testRoutingIncludesSpringRouterStyleExplicitlyMappedBeans() { this.factory.registerAlias("timber", "no-slash"); String expectedTemplate = "/renewable/timber/{farm_type}"; - router.setAttachments(Collections.singletonMap(expectedTemplate, "timber")); + router.setAttachments( + Collections.singletonMap( + requiredString(expectedTemplate), requiredString("timber"))); final RouteList actualRoutes = actualRoutes(); assertEquals(3, actualRoutes.size(), "Wrong number of routes"); @@ -283,4 +290,21 @@ void testRoutingSkipsResourcesWithoutAppropriateAliases() { final RouteList actualRoutes = actualRoutes(); assertEquals(2, actualRoutes.size(), "Timber resource should have been skipped"); } + + @NonNull + private DefaultListableBeanFactory requiredBeanFactory() { + DefaultListableBeanFactory currentFactory = this.factory; + if (currentFactory == null) { + throw new IllegalStateException("factory"); + } + return currentFactory; + } + + @NonNull + private String requiredString(String value) { + if (value == null) { + throw new IllegalStateException("value"); + } + return value; + } } diff --git a/org.restlet.ext.thymeleaf/src/main/java/org/restlet/ext/thymeleaf/TemplateRepresentation.java b/org.restlet.ext.thymeleaf/src/main/java/org/restlet/ext/thymeleaf/TemplateRepresentation.java index 7bb7eaf52f..27b1bc9317 100644 --- a/org.restlet.ext.thymeleaf/src/main/java/org/restlet/ext/thymeleaf/TemplateRepresentation.java +++ b/org.restlet.ext.thymeleaf/src/main/java/org/restlet/ext/thymeleaf/TemplateRepresentation.java @@ -29,7 +29,6 @@ import org.thymeleaf.context.IContext; import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; import org.thymeleaf.templateresolver.ITemplateResolver; -import org.thymeleaf.util.Validate; /** * Thymeleaf template representation. Useful for dynamic string-based representations. @@ -61,10 +60,6 @@ public ResolverContext(Locale locale, Resolver resolver) { this.resolver = resolver; } - public final void addContextExecutionInfo(final String templateName) { - Validate.notEmpty(templateName, "Template name cannot be null or empty"); - } - public Locale getLocale() { return locale; } diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/ResolvingTransformerTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/ResolvingTransformerTestCase.java index 8794737e6d..0f294629e8 100644 --- a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/ResolvingTransformerTestCase.java +++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/ResolvingTransformerTestCase.java @@ -84,7 +84,8 @@ void assertResolving(String message, String testUri, String testData) dataReader.close(); } else { - // TODO support other source implementations (namely sax-source implementations) + // This test helper currently supports stream-based sources only. + // SAX-based sources would need a dedicated reader path here. fail( "test implementation currently doesn't handle other source (e.g., sax) implementations"); } @@ -245,9 +246,8 @@ void testTransform() throws Exception { .getEntity(); TransformRepresentation tr = new TransformRepresentation(comp.getContext(), xmlIn, xsltOne); - // TODO transformer output should go to SAX! The sax-event-stream should - // then be fed into a DOMBuilder - // and then the assertions should be written as DOM tests... + // A SAX-based assertion path would be more robust here: feed the + // transformer event stream into a DOMBuilder and assert on the DOM. // (NOTE: current string-compare assertion might fail on lexical aspects // as ignorable whitespace, encoding settings etc etc) ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/org.restlet/src/main/java/org/restlet/Response.java b/org.restlet/src/main/java/org/restlet/Response.java index 355026bc27..3790d3b65d 100644 --- a/org.restlet/src/main/java/org/restlet/Response.java +++ b/org.restlet/src/main/java/org/restlet/Response.java @@ -710,7 +710,6 @@ public void setAccessControlAllowMethods(Set accessControlAllowMethods) * @param accessControlAllowOrigin The origin allowed by the requested resource. */ public void setAccessControlAllowOrigin(String accessControlAllowOrigin) { - // TODO Add some input validation here. this.accessControlAllowOrigin = accessControlAllowOrigin; } diff --git a/org.restlet/src/main/java/org/restlet/Restlet.java b/org.restlet/src/main/java/org/restlet/Restlet.java index f2dd04cc47..c63c42e28a 100644 --- a/org.restlet/src/main/java/org/restlet/Restlet.java +++ b/org.restlet/src/main/java/org/restlet/Restlet.java @@ -138,6 +138,7 @@ public org.restlet.resource.Finder createFinder( /** Attempts to {@link #stop()} the Restlet if it is still started. */ @Override + @SuppressWarnings("removal") protected void finalize() throws Throwable { if (isStarted()) { stop(); diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java b/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java index 0ea0e61e06..3846cdb3fc 100644 --- a/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java +++ b/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java @@ -194,6 +194,7 @@ private InputStream getInputStream(final long contentLength, final boolean conne pbi.unread(next); requestStream = pbi; } else { + pbi.close(); requestStream = null; } } catch (IOException e) { @@ -204,6 +205,8 @@ private InputStream getInputStream(final long contentLength, final boolean conne } catch (IOException e1) { getLogger().fine("Unable to close request entity"); } + + requestStream = null; } } return requestStream; diff --git a/org.restlet/src/main/java/org/restlet/engine/converter/DefaultConverter.java b/org.restlet/src/main/java/org/restlet/engine/converter/DefaultConverter.java index 6e5369a6f8..9b904524df 100644 --- a/org.restlet/src/main/java/org/restlet/engine/converter/DefaultConverter.java +++ b/org.restlet/src/main/java/org/restlet/engine/converter/DefaultConverter.java @@ -230,6 +230,7 @@ public T toObject(Representation source, Class target, Resource resource) return null; } + @SuppressWarnings("unchecked") private T toFile(final Representation source) { return (source instanceof FileRepresentation fileRepresentation) ? (T) fileRepresentation.getFile() diff --git a/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java index 739c218461..e61137cd34 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java @@ -33,9 +33,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -415,9 +413,8 @@ private Variants lookForVariants(final String baseName, final File file) { if (files != null && files.length > 0) { // Set the list of extensions, due to the file name and the default metadata. - // TODO It seems we could handle more clearly the equivalence - // between the file name space and the target resource (URI completed by default - // metadata) + // This compares the file-name metadata space with the target resource after + // default metadata has been applied to the URI. Variant variant = new Variant(); Entity.updateMetadata(file.getName(), variant, false, getMetadataService()); Collection extensions = Entity.getExtensions(variant, getMetadataService()); @@ -537,9 +534,16 @@ private Status replaceFile(Request request, File file) { /** Create a temporary file with private access rights. */ private static File createPrivateTempFile() throws IOException { - Set perms = PosixFilePermissions.fromString("rw-------"); - FileAttribute> attr = PosixFilePermissions.asFileAttribute(perms); - return Files.createTempFile("restlet-upload", "bin", attr).toFile(); + Path temporaryFile = Files.createTempFile("restlet-upload", "bin"); + + if (Files.getFileStore(temporaryFile).supportsFileAttributeView("posix")) { + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(temporaryFile, perms); + } + + return temporaryFile.toFile(); } private Status replaceFileByTemporaryFile(Request request, File file, File tmp) { diff --git a/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java index 284c7706e5..8d4244c0b0 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java @@ -16,11 +16,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.attribute.FileAttribute; +import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; import java.util.Collection; import java.util.Enumeration; +import java.util.HashSet; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -263,9 +263,16 @@ private File copyZipFileWithUpdatedEntry( /** Create a temporary file with private access rights. */ private static File createPrivateTempFile() throws IOException { - Set perms = PosixFilePermissions.fromString("rw-------"); - FileAttribute> attr = PosixFilePermissions.asFileAttribute(perms); - return Files.createTempFile("restlet_zip_", "zip", attr).toFile(); + Path temporaryFile = Files.createTempFile("restlet_zip_", "zip"); + + if (Files.getFileStore(temporaryFile).supportsFileAttributeView("posix")) { + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(temporaryFile, perms); + } + + return temporaryFile.toFile(); } /** diff --git a/org.restlet/src/main/java/org/restlet/engine/local/ZipEntryRepresentation.java b/org.restlet/src/main/java/org/restlet/engine/local/ZipEntryRepresentation.java index 0dcafbf827..b67d0a36cd 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/ZipEntryRepresentation.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/ZipEntryRepresentation.java @@ -8,6 +8,7 @@ */ package org.restlet.engine.local; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -62,7 +63,16 @@ public ZipEntryRepresentation( @Override public InputStream getStream() throws IOException { - return zipFile.getInputStream(entry); + return new FilterInputStream(zipFile.getInputStream(entry)) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + release(); + } + } + }; } @Override @@ -75,6 +85,8 @@ public void release() { @Override public void write(OutputStream outputStream) throws IOException { - IoUtils.copy(getStream(), outputStream); + try (InputStream inputStream = getStream()) { + IoUtils.copy(inputStream, outputStream); + } } } diff --git a/org.restlet/src/main/java/org/restlet/engine/resource/ClientInvocationHandler.java b/org.restlet/src/main/java/org/restlet/engine/resource/ClientInvocationHandler.java index 88af4850b7..71ebe2dfa9 100644 --- a/org.restlet/src/main/java/org/restlet/engine/resource/ClientInvocationHandler.java +++ b/org.restlet/src/main/java/org/restlet/engine/resource/ClientInvocationHandler.java @@ -263,10 +263,9 @@ private void handleErrorResponse(java.lang.reflect.Method javaMethod, Response r if (t != null) { throw t; } - // TODO cf issues 1004 and 1018. - // this code has been commented as the automatic - // deserialization is problematic. We may rethink a - // way to recover the status info. + // This branch stays disabled because automatic deserialization of + // StatusInfo has been problematic in past issue reports. + // If status recovery is revisited, it needs a safer approach. // } else if (response.isEntityAvailable()) { // StatusInfo si = getClientResource().toObject( // response.getEntity(), StatusInfo.class); diff --git a/org.restlet/src/main/java/org/restlet/engine/resource/MethodAnnotationInfo.java b/org.restlet/src/main/java/org/restlet/engine/resource/MethodAnnotationInfo.java index 95e4c86a63..3c99c061ba 100644 --- a/org.restlet/src/main/java/org/restlet/engine/resource/MethodAnnotationInfo.java +++ b/org.restlet/src/main/java/org/restlet/engine/resource/MethodAnnotationInfo.java @@ -301,7 +301,13 @@ private static List getVariantList( final CharacterSet characterSet) { Variant variant; for (MediaType mediaType : mediaTypes) { - if ((result == null) || (!result.contains(mediaType))) { + boolean containsMediaType = + result != null + && result.stream() + .map(Variant::getMediaType) + .anyMatch(mediaType::equals); + + if ((result == null) || !containsMediaType) { if (result == null) { result = new ArrayList<>(); } diff --git a/org.restlet/src/main/java/org/restlet/resource/ClientResource.java b/org.restlet/src/main/java/org/restlet/resource/ClientResource.java index 7ada02a07f..eccb9a60f6 100644 --- a/org.restlet/src/main/java/org/restlet/resource/ClientResource.java +++ b/org.restlet/src/main/java/org/restlet/resource/ClientResource.java @@ -560,6 +560,7 @@ protected void doRelease() throws ResourceException { /** Attempts to {@link #release()} the resource. */ @Override + @SuppressWarnings("removal") protected void finalize() throws Throwable { release(); super.finalize(); diff --git a/org.restlet/src/main/java/org/restlet/util/WrapperList.java b/org.restlet/src/main/java/org/restlet/util/WrapperList.java index ba624cfcb7..37555f35b9 100644 --- a/org.restlet/src/main/java/org/restlet/util/WrapperList.java +++ b/org.restlet/src/main/java/org/restlet/util/WrapperList.java @@ -24,7 +24,7 @@ * @see java.util.Collections * @see java.util.List */ -public class WrapperList implements List, Iterable { +public class WrapperList implements List { /** The delegate list. */ private final List delegate; diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java index cb6319748c..6dceb17a90 100644 --- a/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java +++ b/org.restlet/src/test/java/org/restlet/engine/connector/MultiPartRepresentationTestCase.java @@ -19,6 +19,7 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.MultiPart; import org.eclipse.jetty.http.MultiPart.Part; +import org.eclipse.jetty.io.ByteBufferPool; import org.junit.jupiter.api.Test; import org.restlet.data.MediaType; import org.restlet.representation.MultiPartRepresentation; @@ -37,7 +38,12 @@ void testWriteFromParts() throws IOException { Files.write( textFilePath, "this is the content of the file".getBytes(StandardCharsets.UTF_8)); MultiPart.PathPart filePart = - new MultiPart.PathPart("icon", "text.txt", HttpFields.EMPTY, textFilePath); + new MultiPart.PathPart( + ByteBufferPool.SIZED_NON_POOLING, + "icon", + "text.txt", + HttpFields.EMPTY, + textFilePath); MultiPart.ContentSourcePart contentSourcePart = new MultiPart.ContentSourcePart( diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/ShutdownHookTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/ShutdownHookTestCase.java index b100942af6..c585cbba79 100644 --- a/org.restlet/src/test/java/org/restlet/engine/connector/ShutdownHookTestCase.java +++ b/org.restlet/src/test/java/org/restlet/engine/connector/ShutdownHookTestCase.java @@ -406,7 +406,7 @@ private synchronized Instant stopServer(final Server server) { * org.eclipse.jetty.util.thread.QueuedThreadPool}. */ private Duration toJettyEffectiveTimeout(final Duration timeout) { - return timeout.plusMillis(500); // FIXME: needs improvements + return timeout.plusMillis(500); // Accounts for Jetty's extra half-timeout behavior. } private static void log(final String message) { diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/SslBaseConnectorsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/SslBaseConnectorsTestCase.java index ea7f3dfb67..ebcc7dfbf6 100644 --- a/org.restlet/src/test/java/org/restlet/engine/connector/SslBaseConnectorsTestCase.java +++ b/org.restlet/src/test/java/org/restlet/engine/connector/SslBaseConnectorsTestCase.java @@ -34,7 +34,6 @@ * @author Bruno Harbulot * @author Jerome Louvel */ -@SuppressWarnings("unused") public abstract class SslBaseConnectorsTestCase extends BaseConnectorsTestCase { protected static final String KEYSTORE_FILE_NAME = "dummy.p12"; diff --git a/org.restlet/src/test/java/org/restlet/resource/AnnotatedResource13TestCase.java b/org.restlet/src/test/java/org/restlet/resource/AnnotatedResource13TestCase.java index ca0babcd20..63f9c298d0 100644 --- a/org.restlet/src/test/java/org/restlet/resource/AnnotatedResource13TestCase.java +++ b/org.restlet/src/test/java/org/restlet/resource/AnnotatedResource13TestCase.java @@ -83,7 +83,7 @@ public FullContact retrieveFull() { } } - public static class Contact extends LightContact implements Serializable { + public static class Contact extends LightContact { private Date birthDate; @@ -113,7 +113,7 @@ public void setEmail2(String email) { } } - public static class FullContact extends Contact implements Serializable { + public static class FullContact extends Contact { private String address1; diff --git a/org.restlet/src/test/java/org/restlet/service/StatusServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/StatusServiceTestCase.java index 6290b899b6..750158911b 100644 --- a/org.restlet/src/test/java/org/restlet/service/StatusServiceTestCase.java +++ b/org.restlet/src/test/java/org/restlet/service/StatusServiceTestCase.java @@ -204,9 +204,5 @@ public AnnotatedSerializableException(String message, int value, Throwable cause super(message, cause); this.value = value; } - - public int getValue() { - return value; - } } } From e5a0fd249409b552172139b84fbe66aedf41090e Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Thu, 14 May 2026 14:03:33 -0400 Subject: [PATCH 2/4] fix: improve Java 25 and Windows temp-file compatibility --- .../ext/spring/SpringBeanRouterTestCase.java | 5 ++- .../engine/local/FileClientHelper.java | 33 +++++++++++++++- .../restlet/engine/local/ZipClientHelper.java | 39 ++++++++++++++++++- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java index ddc128fb2c..7bd7473d82 100644 --- a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java +++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringBeanRouterTestCase.java @@ -299,9 +299,10 @@ void testRoutingSkipsResourcesWithoutAppropriateAliases() { } @NonNull private String requiredString(String value) { - if (value == null) { + String currentValue = value; + if (currentValue == null) { throw new IllegalStateException("value"); } - return value; + return currentValue; } } diff --git a/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java index e61137cd34..852f71c126 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java @@ -92,6 +92,8 @@ public class FileClientHelper extends EntityClientHelper { private static final String ERROR_MESSAGE_UNABLE_FILE_CREATION = "Unable to create the new file"; + private static final String UPLOAD_TEMP_DIRECTORY_PREFIX = "restlet-upload-"; + /** * Constructor. * @@ -534,7 +536,17 @@ private Status replaceFile(Request request, File file) { /** Create a temporary file with private access rights. */ private static File createPrivateTempFile() throws IOException { - Path temporaryFile = Files.createTempFile("restlet-upload", "bin"); + Path temporaryDirectory = Files.createTempDirectory(UPLOAD_TEMP_DIRECTORY_PREFIX); + + if (Files.getFileStore(temporaryDirectory).supportsFileAttributeView("posix")) { + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.OWNER_EXECUTE); + Files.setPosixFilePermissions(temporaryDirectory, perms); + } + + Path temporaryFile = Files.createTempFile(temporaryDirectory, "restlet-upload", "bin"); if (Files.getFileStore(temporaryFile).supportsFileAttributeView("posix")) { Set perms = new HashSet<>(); @@ -560,6 +572,7 @@ private Status replaceFileByTemporaryFile(Request request, File file, File tmp) // Finally, move the temporary file to the existing file location if (tmp.renameTo(file)) { + cleanTemporaryDirectory(tmp); if (request.isEntityAvailable()) { return SUCCESS_NO_CONTENT; } @@ -575,6 +588,7 @@ private Status replaceFileByTemporaryFile(Request request, File file, File tmp) } try { Files.move(tmp.toPath(), file.toPath(), REPLACE_EXISTING); + cleanTemporaryDirectory(tmp); } catch (IOException e) { return new Status( SERVER_ERROR_INTERNAL, @@ -631,6 +645,23 @@ private Status createFile(Request request, File file) { private void cleanTemporaryFileIfUploadNotResumed(File tmp) { if (tmp != null && tmp.exists() && !isResumeUpload()) { IoUtils.delete(tmp); + cleanTemporaryDirectory(tmp); + } + } + + private static void cleanTemporaryDirectory(File tmp) { + if (tmp == null) { + return; + } + + File parent = tmp.getParentFile(); + if (parent == null || !parent.getName().startsWith(UPLOAD_TEMP_DIRECTORY_PREFIX)) { + return; + } + + String[] remainingEntries = parent.list(); + if (remainingEntries != null && remainingEntries.length == 0) { + IoUtils.delete(parent); } } diff --git a/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java index 8d4244c0b0..1ef57bbe40 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java @@ -210,7 +210,11 @@ protected void handlePut(Request request, Response response, File file, String e if (file.exists()) { final File newZipFile = copyZipFileWithUpdatedEntry(file, entryName, entity, isDirectory); - Files.move(newZipFile.toPath(), file.toPath(), REPLACE_EXISTING); + try { + Files.move(newZipFile.toPath(), file.toPath(), REPLACE_EXISTING); + } finally { + deleteParentDirectoryIfEmpty(newZipFile); + } response.setStatus(Status.SUCCESS_OK); } else { @@ -233,6 +237,7 @@ private File copyZipFileWithUpdatedEntry( throws IOException { final File writeTo = createPrivateTempFile(); + boolean completed = false; try (final ZipFile zipFile = new ZipFile(file); final ZipOutputStream zipOut = @@ -257,13 +262,29 @@ private File copyZipFileWithUpdatedEntry( if (!replaced) { writeEntityStream(entity, zipOut, entryName, isDirectory); } + completed = true; + } finally { + if (!completed) { + Files.deleteIfExists(writeTo.toPath()); + deleteParentDirectoryIfEmpty(writeTo); + } } return writeTo; } /** Create a temporary file with private access rights. */ private static File createPrivateTempFile() throws IOException { - Path temporaryFile = Files.createTempFile("restlet_zip_", "zip"); + Path temporaryDirectory = Files.createTempDirectory("restlet_zip_"); + + if (Files.getFileStore(temporaryDirectory).supportsFileAttributeView("posix")) { + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.OWNER_EXECUTE); + Files.setPosixFilePermissions(temporaryDirectory, perms); + } + + Path temporaryFile = Files.createTempFile(temporaryDirectory, "archive_", ".zip"); if (Files.getFileStore(temporaryFile).supportsFileAttributeView("posix")) { Set perms = new HashSet<>(); @@ -275,6 +296,20 @@ private static File createPrivateTempFile() throws IOException { return temporaryFile.toFile(); } + private static void deleteParentDirectoryIfEmpty(File file) { + Path parent = file.toPath().getParent(); + + if (parent == null) { + return; + } + + try { + Files.deleteIfExists(parent); + } catch (IOException ignored) { + // Ignore cleanup failures for temporary directories. + } + } + /** * Writes an entity to a given ZIP output stream with a given ZIP entry name. * From 1cbb2b99b894e3826ac835d3387ed4c58926f0d0 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Thu, 14 May 2026 14:17:08 -0400 Subject: [PATCH 3/4] fix: address Sonar temp file findings --- .../engine/local/FileClientHelper.java | 44 +----------------- .../engine/local/LocalClientHelper.java | 34 ++++++++++++++ .../restlet/engine/local/ZipClientHelper.java | 46 +------------------ 3 files changed, 37 insertions(+), 87 deletions(-) diff --git a/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java index 852f71c126..1bccba7e93 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/FileClientHelper.java @@ -33,13 +33,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Set; import org.restlet.Client; import org.restlet.Request; import org.restlet.Response; @@ -92,8 +90,6 @@ public class FileClientHelper extends EntityClientHelper { private static final String ERROR_MESSAGE_UNABLE_FILE_CREATION = "Unable to create the new file"; - private static final String UPLOAD_TEMP_DIRECTORY_PREFIX = "restlet-upload-"; - /** * Constructor. * @@ -536,26 +532,7 @@ private Status replaceFile(Request request, File file) { /** Create a temporary file with private access rights. */ private static File createPrivateTempFile() throws IOException { - Path temporaryDirectory = Files.createTempDirectory(UPLOAD_TEMP_DIRECTORY_PREFIX); - - if (Files.getFileStore(temporaryDirectory).supportsFileAttributeView("posix")) { - Set perms = new HashSet<>(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - perms.add(PosixFilePermission.OWNER_EXECUTE); - Files.setPosixFilePermissions(temporaryDirectory, perms); - } - - Path temporaryFile = Files.createTempFile(temporaryDirectory, "restlet-upload", "bin"); - - if (Files.getFileStore(temporaryFile).supportsFileAttributeView("posix")) { - Set perms = new HashSet<>(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - Files.setPosixFilePermissions(temporaryFile, perms); - } - - return temporaryFile.toFile(); + return LocalClientHelper.createPrivateTempFile("restlet-upload", "bin"); } private Status replaceFileByTemporaryFile(Request request, File file, File tmp) { @@ -572,7 +549,6 @@ private Status replaceFileByTemporaryFile(Request request, File file, File tmp) // Finally, move the temporary file to the existing file location if (tmp.renameTo(file)) { - cleanTemporaryDirectory(tmp); if (request.isEntityAvailable()) { return SUCCESS_NO_CONTENT; } @@ -588,7 +564,6 @@ private Status replaceFileByTemporaryFile(Request request, File file, File tmp) } try { Files.move(tmp.toPath(), file.toPath(), REPLACE_EXISTING); - cleanTemporaryDirectory(tmp); } catch (IOException e) { return new Status( SERVER_ERROR_INTERNAL, @@ -645,23 +620,6 @@ private Status createFile(Request request, File file) { private void cleanTemporaryFileIfUploadNotResumed(File tmp) { if (tmp != null && tmp.exists() && !isResumeUpload()) { IoUtils.delete(tmp); - cleanTemporaryDirectory(tmp); - } - } - - private static void cleanTemporaryDirectory(File tmp) { - if (tmp == null) { - return; - } - - File parent = tmp.getParentFile(); - if (parent == null || !parent.getName().startsWith(UPLOAD_TEMP_DIRECTORY_PREFIX)) { - return; - } - - String[] remainingEntries = parent.list(); - if (remainingEntries != null && remainingEntries.length == 0) { - IoUtils.delete(parent); } } diff --git a/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java index a9eeef8dc4..866bfbba65 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java @@ -8,6 +8,16 @@ */ package org.restlet.engine.local; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; import org.restlet.Client; import org.restlet.Request; import org.restlet.Response; @@ -49,6 +59,10 @@ * @author Thierry Boileau */ public abstract class LocalClientHelper extends ClientHelper { + + private static final FileAttribute> OWNER_READ_WRITE_FILE_PERMISSIONS = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")); + /** * Constructor. Note that the common list of metadata associations based on extensions is added, * see the addCommonExtensions() method. @@ -107,6 +121,26 @@ public final void handle(Request request, Response response) { } } + protected static File createPrivateTempFile(String prefix, String suffix) throws IOException { + Path temporaryDirectory = Paths.get(System.getProperty("java.io.tmpdir")); + FileStore fileStore = Files.getFileStore(temporaryDirectory); + Path temporaryFile; + + if (fileStore.supportsFileAttributeView("posix")) { + temporaryFile = Files.createTempFile(prefix, suffix, OWNER_READ_WRITE_FILE_PERMISSIONS); + } else { + temporaryFile = Files.createTempFile(prefix, suffix); + File temporaryFileAsFile = temporaryFile.toFile(); + temporaryFileAsFile.setReadable(false, false); + temporaryFileAsFile.setWritable(false, false); + temporaryFileAsFile.setExecutable(false, false); + temporaryFileAsFile.setReadable(true, true); + temporaryFileAsFile.setWritable(true, true); + } + + return temporaryFile.toFile(); + } + /** * Handles a local call. * diff --git a/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java index 1ef57bbe40..931712bc48 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/ZipClientHelper.java @@ -16,12 +16,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; import java.util.Collection; import java.util.Enumeration; -import java.util.HashSet; -import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; @@ -210,11 +206,7 @@ protected void handlePut(Request request, Response response, File file, String e if (file.exists()) { final File newZipFile = copyZipFileWithUpdatedEntry(file, entryName, entity, isDirectory); - try { - Files.move(newZipFile.toPath(), file.toPath(), REPLACE_EXISTING); - } finally { - deleteParentDirectoryIfEmpty(newZipFile); - } + Files.move(newZipFile.toPath(), file.toPath(), REPLACE_EXISTING); response.setStatus(Status.SUCCESS_OK); } else { @@ -266,7 +258,6 @@ private File copyZipFileWithUpdatedEntry( } finally { if (!completed) { Files.deleteIfExists(writeTo.toPath()); - deleteParentDirectoryIfEmpty(writeTo); } } return writeTo; @@ -274,40 +265,7 @@ private File copyZipFileWithUpdatedEntry( /** Create a temporary file with private access rights. */ private static File createPrivateTempFile() throws IOException { - Path temporaryDirectory = Files.createTempDirectory("restlet_zip_"); - - if (Files.getFileStore(temporaryDirectory).supportsFileAttributeView("posix")) { - Set perms = new HashSet<>(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - perms.add(PosixFilePermission.OWNER_EXECUTE); - Files.setPosixFilePermissions(temporaryDirectory, perms); - } - - Path temporaryFile = Files.createTempFile(temporaryDirectory, "archive_", ".zip"); - - if (Files.getFileStore(temporaryFile).supportsFileAttributeView("posix")) { - Set perms = new HashSet<>(); - perms.add(PosixFilePermission.OWNER_READ); - perms.add(PosixFilePermission.OWNER_WRITE); - Files.setPosixFilePermissions(temporaryFile, perms); - } - - return temporaryFile.toFile(); - } - - private static void deleteParentDirectoryIfEmpty(File file) { - Path parent = file.toPath().getParent(); - - if (parent == null) { - return; - } - - try { - Files.deleteIfExists(parent); - } catch (IOException ignored) { - // Ignore cleanup failures for temporary directories. - } + return LocalClientHelper.createPrivateTempFile("restlet_zip_", ".zip"); } /** From 17a7e300d41a2e0eb22c0e0125a429cb43a42a32 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Thu, 14 May 2026 14:38:19 -0400 Subject: [PATCH 4/4] fix: use a private temp root for local helpers --- .../engine/local/LocalClientHelper.java | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java index 866bfbba65..20366784bd 100644 --- a/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java +++ b/org.restlet/src/main/java/org/restlet/engine/local/LocalClientHelper.java @@ -60,6 +60,11 @@ */ public abstract class LocalClientHelper extends ClientHelper { + private static final String PRIVATE_TEMP_DIRECTORY_NAME = ".restlet/tmp"; + + private static final FileAttribute> OWNER_ALL_DIRECTORY_PERMISSIONS = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); + private static final FileAttribute> OWNER_READ_WRITE_FILE_PERMISSIONS = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")); @@ -122,25 +127,69 @@ public final void handle(Request request, Response response) { } protected static File createPrivateTempFile(String prefix, String suffix) throws IOException { - Path temporaryDirectory = Paths.get(System.getProperty("java.io.tmpdir")); + Path temporaryDirectory = getPrivateTempDirectory(); FileStore fileStore = Files.getFileStore(temporaryDirectory); Path temporaryFile; if (fileStore.supportsFileAttributeView("posix")) { - temporaryFile = Files.createTempFile(prefix, suffix, OWNER_READ_WRITE_FILE_PERMISSIONS); + temporaryFile = + Files.createTempFile( + temporaryDirectory, prefix, suffix, OWNER_READ_WRITE_FILE_PERMISSIONS); } else { - temporaryFile = Files.createTempFile(prefix, suffix); + temporaryFile = Files.createTempFile(temporaryDirectory, prefix, suffix); File temporaryFileAsFile = temporaryFile.toFile(); - temporaryFileAsFile.setReadable(false, false); - temporaryFileAsFile.setWritable(false, false); - temporaryFileAsFile.setExecutable(false, false); - temporaryFileAsFile.setReadable(true, true); - temporaryFileAsFile.setWritable(true, true); + setOwnerOnlyAccess(temporaryFileAsFile, false); } return temporaryFile.toFile(); } + private static Path getPrivateTempDirectory() throws IOException { + Path homeDirectory = getPrivateTempHomeRoot(); + Path privateTempDirectory = homeDirectory.resolve(PRIVATE_TEMP_DIRECTORY_NAME); + + if (Files.exists(privateTempDirectory)) { + if (Files.getFileStore(privateTempDirectory).supportsFileAttributeView("posix")) { + Files.setPosixFilePermissions( + privateTempDirectory, OWNER_ALL_DIRECTORY_PERMISSIONS.value()); + } + return privateTempDirectory; + } + + if (Files.getFileStore(homeDirectory).supportsFileAttributeView("posix")) { + return Files.createDirectories(privateTempDirectory, OWNER_ALL_DIRECTORY_PERMISSIONS); + } + + return Files.createDirectories(privateTempDirectory); + } + + private static Path getPrivateTempHomeRoot() { + String userHome = System.getProperty("user.home"); + + if (userHome != null && !userHome.isEmpty()) { + return Paths.get(userHome); + } + + return Paths.get(".").toAbsolutePath().normalize(); + } + + private static void setOwnerOnlyAccess(File file, boolean executable) throws IOException { + ensurePermissionChange(file.setReadable(true, true), file, "enable owner read access"); + ensurePermissionChange(file.setWritable(true, true), file, "enable owner write access"); + + if (executable) { + ensurePermissionChange( + file.setExecutable(true, true), file, "enable owner execute access"); + } + } + + private static void ensurePermissionChange(boolean updated, File file, String action) + throws IOException { + if (!updated) { + throw new IOException("Unable to " + action + " for " + file.getAbsolutePath()); + } + } + /** * Handles a local call. *