This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 9882259 JUNEAU-142, JUNEAU-143
9882259 is described below
commit 98822599ea5ca9bd267b125681997b1076c4675b
Author: JamesBognar <[email protected]>
AuthorDate: Sun Sep 8 11:28:09 2019 -0400
JUNEAU-142, JUNEAU-143
Static files mapping doesn't properly handle override scenarios.
Add support for absolute paths on @RestResource(staticFiles)
---
.../org/apache/juneau/PropertyStoreBuilder.java | 6 +-
.../utils/ClasspathResourceFinderSimple.java | 15 +---
juneau-doc/docs/ReleaseNotes/8.1.1.html | 9 +++
.../07.juneau-rest-server/26.StaticFiles.html | 2 +-
juneau-doc/docs/docs.txt | 1 +
.../annotation/RestResourceStaticFilesTest.java | 48 +++++++++++-
.../java/org/apache/juneau/rest/RestContext.java | 87 +++++++++++++++-------
.../org/apache/juneau/rest/StaticFileMapping.java | 36 +++++++--
.../java/org/apache/juneau/rest/StaticFiles.java | 76 +++++++++++++++++++
9 files changed, 232 insertions(+), 48 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
index 55d9f02..05fd053 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
@@ -341,6 +341,7 @@ public class PropertyStoreBuilder {
* Out-of-range indexes are simply 'adjusted' to the
beginning or the end of the list.
* So, for example, a value of <js>"-100"</js> will always
just cause the entry to be added to the beginning
* of the list.
+ * <br>NOTE: If <jk>null</jk>, value will be inserted at
position 0.
* <br>For MAPs, this can be <jk>null</jk> if we're adding a map,
or a string key if we're adding an entry.
* @param value
* The new value to add to the property.
@@ -370,11 +371,14 @@ public class PropertyStoreBuilder {
}
/**
- * Adds a value to a SET, LIST, or MAP property.
+ * Adds/prepends a value to a SET, LIST, or MAP property.
*
* <p>
* Shortcut for calling <code>addTo(key, <jk>null</jk>, value);</code>.
*
+ * <p>
+ * NOTE: When adding to a list, the value is inserted at the beginning
of the list.
+ *
* @param key The property key.
* @param value
* The new value to add to the property.
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
index 7eea5f3..eb97c8f 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
@@ -65,10 +65,10 @@ public class ClasspathResourceFinderSimple implements
ClasspathResourceFinder {
* @throws IOException Thrown by underlying stream.
*/
protected InputStream findClasspathResource(Class<?> baseClass, String
name, Locale locale) throws IOException {
-
- if (locale == null)
+
+ if (locale == null)
return getResourceAsStream(baseClass, name);
-
+
for (String n : getCandidateFileNames(name, locale)) {
InputStream is = getResourceAsStream(baseClass, n);
if (is != null)
@@ -78,14 +78,7 @@ public class ClasspathResourceFinderSimple implements
ClasspathResourceFinder {
}
private InputStream getResourceAsStream(Class<?> baseClass, String
name) {
- InputStream is = baseClass.getResourceAsStream(name);
- if (is != null)
- return is;
- if (! name.startsWith("/"))
- is = baseClass.getResourceAsStream("/" + name);
- if (is != null)
- return is;
- return null;
+ return baseClass.getResourceAsStream(name);
}
/**
diff --git a/juneau-doc/docs/ReleaseNotes/8.1.1.html
b/juneau-doc/docs/ReleaseNotes/8.1.1.html
index fe2213d..ca60e8f 100644
--- a/juneau-doc/docs/ReleaseNotes/8.1.1.html
+++ b/juneau-doc/docs/ReleaseNotes/8.1.1.html
@@ -74,6 +74,15 @@
<li class='jm'>{@link
oajr.BasicRest#log(Level,Throwable,String,Object[])
log(Level,Throwable,String,Object[])}
</ul>
</ul>
+ <li>
+ The <c>@RestResource(staticFiles)</c> annotation now supports
absolute path locations:
+ <p class='bpcode w800'>
+ <jc>// Resolves static files in root package "htdocs" or working
directory "htdocs".
+ <ja>@RestResource</ja>(staticFiles=<js>"htdocs:/htdocs"</js>)
+ </p>
+ <li>
+ Fixed a bug in <c>@RestResource(staticFiles)</c> where the
order of lookup between parent and child resources
+ was wrong.
</ul>
<h5 class='topic w800'>juneau-rest-client</h5>
diff --git a/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
b/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
index bf55663..dcf82d7 100644
--- a/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
+++ b/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
@@ -13,7 +13,7 @@
***************************************************************************************************************************/
-->
-Static files
+{todo} Static files
<p>
The {@link oajr.annotation.RestResource#staticFiles
@RestResource(staticFiles)}
diff --git a/juneau-doc/docs/docs.txt b/juneau-doc/docs/docs.txt
index 56d6f0b..4563d0f 100644
--- a/juneau-doc/docs/docs.txt
+++ b/juneau-doc/docs/docs.txt
@@ -4,6 +4,7 @@ PojoCategories = #juneau-marshall.PojoCategories, POJO
Categories
PojosConveribleToStrings = #PojosConveribleToStrings, POJOs Convertible
to/from Strings
PojosConveribleToOtherTypes = #PojosConveribleToOtherTypes, POJOs Convertible
to/from Other Types
ConfigurableProperties = #juneau-marshall.ConfigurableProperties, Configurable
Properties
+SimpleJson = #juneau-marshall.JsonDetails.SimplifiedJson, Simple JSON
SwaggerIO.v2 = https://swagger.io/specification/v2
SwaggerIO.v3 = https://swagger.io/specification
diff --git
a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
index 2a5b5e3..f9817b3 100644
---
a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
+++
b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
@@ -23,9 +23,9 @@ import org.junit.runners.*;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class RestResourceStaticFilesTest {
-
//====================================================================================================
+
//------------------------------------------------------------------------------------------------------------------
// Basic tests
-
//====================================================================================================
+
//------------------------------------------------------------------------------------------------------------------
@RestResource(staticFiles={"xdocs:xdocs","xdocs2:xdocs2:{Foo:'Bar'}"})
public static class A {
@@ -47,9 +47,9 @@ public class RestResourceStaticFilesTest {
a.get("/xdocs/xsubdocs/%2E%2E/test.txt?noTrace=true").execute().assertStatus(404);
}
-
//====================================================================================================
+
//------------------------------------------------------------------------------------------------------------------
// Static files with response headers.
-
//====================================================================================================
+
//------------------------------------------------------------------------------------------------------------------
@RestResource(staticFiles={"xdocs:xdocs:{Foo:'Bar'}"})
public static class B {
@@ -64,4 +64,44 @@ public class RestResourceStaticFilesTest {
public void b01() throws Exception {
b.get("/xdocs/test.txt").execute().assertHeader("Foo","Bar").assertBodyContains("OK-1");
}
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Class hierarchy
+
//------------------------------------------------------------------------------------------------------------------
+
+ @RestResource(staticFiles={"xdocs:xdocs"})
+ public static class C1 {
+ @RestMethod
+ public String c01() {
+ return null;
+ }
+ }
+
+ @RestResource(staticFiles={"xdocs:/xdocs"})
+ public static class C2 extends C1 {
+ @RestMethod
+ public String c02() {
+ return null;
+ }
+ }
+
+ static MockRest c1 = MockRest.build(C1.class);
+ static MockRest c2 = MockRest.build(C2.class);
+
+ @Test
+ public void c01() throws Exception {
+ // Should resolve to relative xdocs folder.
+ c1.get("/xdocs/test.txt").execute().assertBodyContains("OK-1");
+
c1.get("/xdocs/xsubdocs/test.txt").execute().assertBodyContains("OK-2");
+
+ // Should be overridden to absolute xdocs folder.
+ c2.get("/xdocs/test.txt").execute().assertBodyContains("OK-3");
+
c2.get("/xdocs/xsubdocs/test.txt").execute().assertBodyContains("OK-4");
+
+ // Should pick up from file system.
+ c1.get("/xdocs/test2.txt").execute().assertBodyContains("OK-5");
+ c2.get("/xdocs/test2.txt").execute().assertBodyContains("OK-5");
+
c1.get("/xdocs/xsubdocs/test2.txt").execute().assertBodyContains("OK-6");
+
c2.get("/xdocs/xsubdocs/test2.txt").execute().assertBodyContains("OK-6");
+ }
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 7291510..15b3fd7 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -50,7 +50,6 @@ import org.apache.juneau.http.annotation.Query;
import org.apache.juneau.http.annotation.Response;
import org.apache.juneau.httppart.*;
import org.apache.juneau.httppart.bean.*;
-import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.jsonschema.*;
import org.apache.juneau.msgpack.*;
@@ -2755,6 +2754,16 @@ public final class RestContext extends BeanContext {
* }
* </p>
*
+ * <p>
+ * Note that headers can also be specified per path-mapping via the
{@link RestResource#staticFiles() @RestResource(staticFiles)} annotation.
+ * <p class='bcode w800'>
+ * <ja>@RestResource</ja>(
+ * staticFiles={
+ *
<js>"htdocs:docs:{'Cache-Control':'max-age=86400, public'}"</js>
+ * }
+ * )
+ * </p>
+ *
* <ul class='seealso'>
* <li class='jf'>{@link #REST_staticFiles} for information about
statically-served files.
* </ul>
@@ -2790,6 +2799,13 @@ public final class RestContext extends BeanContext {
* from the classpath or file system.
*
* <p>
+ * The format of the value is one of the following:
+ * <ol class='spaced-list'>
+ * <li><js>"path:location"</js>
+ * <li><js>"path:location:headers"</js>
+ * </ol>
+ *
+ * <p>
* An example where this class is used is in the {@link
RestResource#staticFiles} annotation:
* <p class='bcode w800'>
* <jk>package</jk> com.foo.mypackage;
@@ -2809,12 +2825,41 @@ public final class RestContext extends BeanContext {
* <p class='bcode w800'>
* /myresource/htdocs/foobar.html
* </p>
- * <br>...the servlet will attempt to find the <c>foobar.html</c> file
in the following ordered locations:
+ * <br>...the servlet will attempt to find the <c>foobar.html</c> file
in the following location:
* <ol class='spaced-list'>
* <li><c>com.foo.mypackage.docs</c> package.
- * <li><c>[working-dir]/docs</c> directory.
* </ol>
*
+ * <p>
+ * The location is interpreted as an absolute path if it starts with
<js>'/'</js>.
+ * <p class='bcode w800'>
+ * <ja>@RestResource</ja>(
+ * staticFiles={
+ * <js>"htdocs:/docs"</js>
+ * }
+ * )
+ * </p>
+ * <p>
+ * In the example above, given a GET request to the following URL...
+ * <p class='bcode w800'>
+ * /myresource/htdocs/foobar.html
+ * </p>
+ * <br>...the servlet will attempt to find the <c>foobar.html</c> file
in the following location:
+ * <ol class='spaced-list'>
+ * <li><c>docs</c> package (typically under
<c>src/main/resources/docs</c> in your workspace).
+ * <li><c>[working-dir]/docs</c> directory at runtime.
+ * </ol>
+ *
+ * <p>
+ * Response headers can be specified for served files by adding a 3rd
section that consists of a {@doc SimpleJson} object.
+ * <p class='bcode w800'>
+ * <ja>@RestResource</ja>(
+ * staticFiles={
+ *
<js>"htdocs:docs:{'Cache-Control':'max-age=86400, public'}"</js>
+ * }
+ * )
+ * </p>
+ *
* <ul class='seealso'>
* <li class='jf'>{@link #REST_classpathResourceFinder} for
configuring how classpath resources are located and retrieved.
* <li class='jf'>{@link #REST_mimeTypes} for configuring the
media types based on file extension.
@@ -2828,6 +2873,8 @@ public final class RestContext extends BeanContext {
* Mappings are cumulative from super classes.
* <li>
* Child resources can override mappings made on parent
class resources.
+ * <br>When both parent and child resources map against
the same path, files will be search in the child location
+ * and then the parent location.
* </ul>
*/
public static final String REST_staticFiles = PREFIX +
".staticFiles.lo";
@@ -3485,7 +3532,7 @@ public final class RestContext extends BeanContext {
private final ObjectMap defaultRequestAttributes;
private final ResponseHandler[] responseHandlers;
private final MimetypesFileTypeMap mimetypesFileTypeMap;
- private final StaticFileMapping[] staticFiles;
+ private final StaticFiles[] staticFiles;
private final String[] staticFilesPaths;
private final MessageBundle msgs;
private final Config config;
@@ -3688,10 +3735,14 @@ public final class RestContext extends BeanContext {
consumes = getListProperty(REST_consumes,
MediaType.class, parsers.getSupportedMediaTypes());
produces = getListProperty(REST_produces,
MediaType.class, serializers.getSupportedMediaTypes());
- staticFiles =
ArrayUtils.reverse(getArrayProperty(REST_staticFiles, StaticFileMapping.class));
+ StaticFileMapping[] staticFileMappings =
getArrayProperty(REST_staticFiles, StaticFileMapping.class, new
StaticFileMapping[0]);
+ staticFiles = new
StaticFiles[staticFileMappings.length];
+ for (int i = 0; i < staticFiles.length; i++)
+ staticFiles[i] = new
StaticFiles(staticFileMappings[i], staticResourceManager, mimetypesFileTypeMap,
staticFileResponseHeaders);
+
Set<String> s = new TreeSet<>();
- for (StaticFileMapping sfm : staticFiles)
- s.add(sfm.path);
+ for (StaticFiles sf : staticFiles)
+ s.add(sf.getPath());
staticFilesPaths = s.toArray(new String[s.size()]);
MessageBundleLocation[] mbl =
getInstanceArrayProperty(REST_messages, MessageBundleLocation.class, new
MessageBundleLocation[0]);
@@ -4085,24 +4136,10 @@ public final class RestContext extends BeanContext {
if (p.indexOf("..") != -1)
throw new NotFound("Invalid path");
StreamResource sr = null;
- for (StaticFileMapping sfm : staticFiles) {
- String path = sfm.path;
- if (p.startsWith(path)) {
- String remainder = (p.equals(path) ? ""
: p.substring(path.length()));
- if (remainder.isEmpty() ||
remainder.startsWith("/")) {
- String p2 = sfm.location +
remainder;
- try (InputStream is =
getClasspathResource(sfm.resourceClass, p2, null)) {
- if (is != null) {
- int i =
p2.lastIndexOf('/');
- String name =
(i == -1 ? p2 : p2.substring(i+1));
- String
mediaType = mimetypesFileTypeMap.getContentType(name);
-
Map<String,Object> responseHeaders = sfm.responseHeaders != null ?
sfm.responseHeaders : staticFileResponseHeaders;
- sr = new
StreamResource(MediaType.forString(mediaType), responseHeaders, true, is);
- break;
- }
- }
- }
- }
+ for (StaticFiles sf : staticFiles) {
+ sr = sf.resolve(p);
+ if (sr != null)
+ break;
}
StaticFile sf = new StaticFile(sr);
if (useClasspathResourceCaching) {
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
index 2bdec39..3fa0424 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
@@ -13,11 +13,12 @@
package org.apache.juneau.rest;
import static org.apache.juneau.internal.CollectionUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
import java.util.*;
import org.apache.juneau.*;
-import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.utils.*;
@@ -86,8 +87,8 @@ public class StaticFileMapping {
*/
public StaticFileMapping(Class<?> resourceClass, String path, String
location, Map<String,Object> responseHeaders) {
this.resourceClass = resourceClass;
- this.path = StringUtils.trimSlashes(path);
- this.location = StringUtils.trimSlashes(location);
+ this.path = trimSlashes(path);
+ this.location = trimTrailingSlashes(location);
this.responseHeaders = immutableMap(responseHeaders);
}
@@ -109,11 +110,11 @@ public class StaticFileMapping {
*/
public StaticFileMapping(Class<?> resourceClass, String mappingString) {
this.resourceClass = resourceClass;
- String[] parts = StringUtils.split(mappingString, ':', 3);
+ String[] parts = split(mappingString, ':', 3);
if (parts == null || parts.length <= 1)
throw new FormattedRuntimeException("Invalid mapping
string format: ''{0}'' on resource class ''{1}''", mappingString,
resourceClass.getName());
- this.path = StringUtils.trimSlashes(parts[0]);
- this.location = StringUtils.trimSlashes(parts[1]);
+ this.path = trimSlashes(parts[0]);
+ this.location = trimTrailingSlashes(parts[1]);
if (parts.length == 3) {
try {
responseHeaders = unmodifiableMap(new
ObjectMap(parts[2]));
@@ -124,4 +125,27 @@ public class StaticFileMapping {
responseHeaders = null;
}
}
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Other methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Override /* Object */
+ public String toString() {
+ return SimpleJsonSerializer.DEFAULT_READABLE.toString(toMap());
+ }
+
+ /**
+ * Returns the properties defined on this bean as a simple map for
debugging purposes.
+ *
+ * @return A new map containing the properties defined on this bean.
+ */
+ public ObjectMap toMap() {
+ return new DefaultFilteringObjectMap()
+ .append("resourceClass", resourceClass)
+ .append("path", path)
+ .append("location", location)
+ .append("responseHeaders", responseHeaders)
+ ;
+ }
}
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
new file mode 100644
index 0000000..e7d859d
--- /dev/null
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
@@ -0,0 +1,76 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you 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
*
+// *
*
+// * http://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. *
+//
***************************************************************************************************************************
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you 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
*
+// *
*
+// * http://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.apache.juneau.rest;
+
+import java.io.*;
+import java.util.*;
+
+import javax.activation.*;
+
+import org.apache.juneau.http.*;
+import org.apache.juneau.utils.*;
+
+/**
+ * The static file resource resolver for a single {@link StaticFileMapping}.
+ */
+class StaticFiles {
+ private final Class<?> resourceClass;
+ private final String path, location;
+ private final Map<String,Object> responseHeaders;
+
+ private final ClasspathResourceManager staticResourceManager;
+ private final MimetypesFileTypeMap mimetypesFileTypeMap;
+
+ StaticFiles(StaticFileMapping sfm, ClasspathResourceManager
staticResourceManager, MimetypesFileTypeMap mimetypesFileTypeMap,
Map<String,Object> staticFileResponseHeaders) {
+ this.resourceClass = sfm.resourceClass;
+ this.path = sfm.path;
+ this.location = sfm.location;
+ this.responseHeaders = sfm.responseHeaders != null ?
sfm.responseHeaders : staticFileResponseHeaders;
+ this.staticResourceManager = staticResourceManager;
+ this.mimetypesFileTypeMap = mimetypesFileTypeMap;
+ }
+
+ String getPath() {
+ return path;
+ }
+
+ StreamResource resolve(String p) throws IOException {
+ if (p.startsWith(path)) {
+ String remainder = (p.equals(path) ? "" :
p.substring(path.length()));
+ if (remainder.isEmpty() || remainder.startsWith("/")) {
+ String p2 = location + remainder;
+ try (InputStream is =
staticResourceManager.getStream(resourceClass, p2, null)) {
+ if (is != null) {
+ int i = p2.lastIndexOf('/');
+ String name = (i == -1 ? p2 :
p2.substring(i+1));
+ String mediaType =
mimetypesFileTypeMap.getContentType(name);
+ return new
StreamResource(MediaType.forString(mediaType), responseHeaders, true, is);
+ }
+ }
+ }
+ }
+ return null;
+ }
+}