GUAC-587: Implement support for serving of arbitrary resources.

This commit is contained in:
Michael Jumper
2015-05-09 22:36:57 -07:00
parent 5755e800d3
commit 4a91f57abb
7 changed files with 657 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.resource;
/**
* Base abstract resource implementation which provides an associated mimetype,
* and modification time. Classes which extend AbstractResource must provide
* their own InputStream, however.
*
* @author Michael Jumper
*/
public abstract class AbstractResource implements Resource {
/**
* The mimetype of this resource.
*/
private final String mimetype;
/**
* The time this resource was last modified, in milliseconds since midnight
* of January 1, 1970 UTC.
*/
private final long lastModified;
/**
* Initializes this AbstractResource with the given mimetype and
* modification time.
*
* @param mimetype
* The mimetype of this resource.
*
* @param lastModified
* The time this resource was last modified, in milliseconds since
* midnight of January 1, 1970 UTC.
*/
public AbstractResource(String mimetype, long lastModified) {
this.mimetype = mimetype;
this.lastModified = lastModified;
}
/**
* Initializes this AbstractResource with the given mimetype. The
* modification time of the resource is set to the current system time.
*
* @param mimetype
* The mimetype of this resource.
*/
public AbstractResource(String mimetype) {
this(mimetype, System.currentTimeMillis());
}
@Override
public long getLastModified() {
return lastModified;
}
@Override
public String getMimeType() {
return mimetype;
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.resource;
import java.io.InputStream;
/**
* A resource which is located within the classpath of an arbitrary
* ClassLoader.
*
* @author Michael Jumper
*/
public class ClassPathResource extends AbstractResource {
/**
* The classloader to use when reading this resource.
*/
private final ClassLoader classLoader;
/**
* The path of this resource relative to the classloader.
*/
private final String path;
/**
* Creates a new ClassPathResource which uses the given ClassLoader to
* read the resource having the given path.
*
* @param classLoader
* The ClassLoader to use when reading the resource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param path
* The path of the resource relative to the given ClassLoader.
*/
public ClassPathResource(ClassLoader classLoader, String mimetype, String path) {
super(mimetype);
this.classLoader = classLoader;
this.path = path;
}
/**
* Creates a new ClassPathResource which uses the ClassLoader associated
* with the ClassPathResource class to read the resource having the given
* path.
*
* @param mimetype
* The mimetype of the resource.
*
* @param path
* The path of the resource relative to the ClassLoader associated
* with the ClassPathResource class.
*/
public ClassPathResource(String mimetype, String path) {
this(ClassPathResource.class.getClassLoader(), mimetype, path);
}
@Override
public InputStream asStream() {
return classLoader.getResourceAsStream(path);
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.resource;
import java.io.InputStream;
/**
* An arbitrary resource that can be served to a user via HTTP. Resources are
* anonymous but have a defined mimetype and corresponding input stream.
*
* @author Michael Jumper
*/
public interface Resource {
/**
* Returns the mimetype of this resource. This function MUST always return
* a value. If the type is unknown, return "application/octet-stream".
*
* @return
* The mimetype of this resource.
*/
String getMimeType();
/**
* Returns the time the resource was last modified in milliseconds since
* midnight of January 1, 1970 UTC.
*
* @return
* The time the resource was last modified, in milliseconds.
*/
long getLastModified();
/**
* Returns an InputStream which reads the contents of this resource,
* starting with the first byte. Reading from the returned InputStream will
* not affect reads from other InputStreams returned by other calls to
* asStream(). The returned InputStream must be manually closed when no
* longer needed. If the resource is unexpectedly unavailable, this will
* return null.
*
* @return
* An InputStream which reads the contents of this resource, starting
* with the first byte, or null if the resource is unavailable.
*/
InputStream asStream();
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Servlet which serves a given resource for all HTTP GET requests. The HEAD
* method is correctly supported, and HTTP 304 ("Not Modified") responses will
* be properly returned for GET requests depending on the last time the
* resource was modified.
*
* @author Michael Jumper
*/
public class ResourceServlet extends HttpServlet {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ResourceServlet.class);
/**
* The size of the buffer to use when transferring data from the input
* stream of a resource to the output stream of a request.
*/
private static final int BUFFER_SIZE = 10240;
/**
* The resource to serve for every GET request.
*/
private final Resource resource;
/**
* Creates a new ResourceServlet which serves the given Resource for all
* HTTP GET requests.
*
* @param resource
* The Resource to serve.
*/
public ResourceServlet(Resource resource) {
this.resource = resource;
}
@Override
protected void doHead(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Set last modified and content type headers
response.addDateHeader("Last-Modified", resource.getLastModified());
response.setContentType(resource.getMimeType());
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Get input stream from resource
InputStream input = resource.asStream();
// If resource does not exist, return not found
if (input == null) {
logger.debug("Resource does not exist: \"{}\"", request.getServletPath());
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
try {
// Write headers
doHead(request, response);
// If not modified since "If-Modified-Since" header, return not modified
long ifModifiedSince = request.getDateHeader("If-Modified-Since");
if (resource.getLastModified() - ifModifiedSince < 1000) {
logger.debug("Resource not modified: \"{}\"", request.getServletPath());
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
int length;
byte[] buffer = new byte[BUFFER_SIZE];
// Write resource to response body
OutputStream output = response.getOutputStream();
while ((length = input.read(buffer)) != -1)
output.write(buffer, 0, length);
}
// Ensure input stream is always closed
finally {
input.close();
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.resource;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
/**
* A resource which is the logical concatenation of other resources.
*
* @author Michael Jumper
*/
public class SequenceResource extends AbstractResource {
/**
* The resources to be concatenated.
*/
private final Iterable<Resource> resources;
/**
* Returns the mimetype of the first resource in the given Iterable, or
* "application/octet-stream" if no resources are provided.
*
* @param resources
* The resources from which the mimetype should be retrieved.
*
* @return
* The mimetype of the first resource, or "application/octet-stream"
* if no resources were provided.
*/
private static String getMimeType(Iterable<Resource> resources) {
// If no resources, just assume application/octet-stream
Iterator<Resource> resourceIterator = resources.iterator();
if (!resourceIterator.hasNext())
return "application/octet-stream";
// Return mimetype of first resource
return resourceIterator.next().getMimeType();
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(String mimetype, Iterable<Resource> resources) {
super(mimetype);
this.resources = resources;
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource. The
* mimetype of the resulting concatenation is derived from the first
* resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(Iterable<Resource> resources) {
super(getMimeType(resources));
this.resources = resources;
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(String mimetype, Resource... resources) {
this(mimetype, Arrays.asList(resources));
}
/**
* Creates a new SequenceResource as the logical concatenation of the
* given resources. Each resource is concatenated in iteration order as
* needed when reading from the input stream of the SequenceResource. The
* mimetype of the resulting concatenation is derived from the first
* resource.
*
* @param resources
* The resources to concatenate within the InputStream of this
* SequenceResource.
*/
public SequenceResource(Resource... resources) {
this(Arrays.asList(resources));
}
@Override
public InputStream asStream() {
return new SequenceInputStream(new Enumeration<InputStream>() {
/**
* Iterator over all resources associated with this
* SequenceResource.
*/
private final Iterator<Resource> resourceIterator = resources.iterator();
@Override
public boolean hasMoreElements() {
return resourceIterator.hasNext();
}
@Override
public InputStream nextElement() {
return resourceIterator.next().asStream();
}
});
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.glyptodon.guacamole.net.basic.resource;
import java.io.InputStream;
import javax.servlet.ServletContext;
/**
* A resource which is located within the classpath associated with another
* class.
*
* @author Michael Jumper
*/
public class WebApplicationResource extends AbstractResource {
/**
* The servlet context to use when reading the resource and, if necessary,
* when determining the mimetype of the resource.
*/
private final ServletContext context;
/**
* The path of this resource relative to the ServletContext.
*/
private final String path;
/**
* Derives a mimetype from the filename within the given path using the
* given ServletContext, if possible.
*
* @param context
* The ServletContext to use to derive the mimetype.
*
* @param path
* The path to derive the mimetype from.
*
* @return
* An appropriate mimetype based on the name of the file in the path,
* or "application/octet-stream" if no mimetype could be determined.
*/
private static String getMimeType(ServletContext context, String path) {
// If mimetype is known, use defined mimetype
String mimetype = context.getMimeType(path);
if (mimetype != null)
return mimetype;
// Otherwise, default to application/octet-stream
return "application/octet-stream";
}
/**
* Creates a new WebApplicationResource which serves the resource at the
* given path relative to the given ServletContext. Rather than deriving
* the mimetype of the resource from the filename within the path, the
* mimetype given is used.
*
* @param context
* The ServletContext to use when reading the resource.
*
* @param mimetype
* The mimetype of the resource.
*
* @param path
* The path of the resource relative to the given ServletContext.
*/
public WebApplicationResource(ServletContext context, String mimetype, String path) {
super(mimetype);
this.context = context;
this.path = path;
}
/**
* Creates a new WebApplicationResource which serves the resource at the
* given path relative to the given ServletContext. The mimetype of the
* resource is automatically determined based on the filename within the
* path.
*
* @param context
* The ServletContext to use when reading the resource and deriving the
* mimetype.
*
* @param path
* The path of the resource relative to the given ServletContext.
*/
public WebApplicationResource(ServletContext context, String path) {
this(context, getMimeType(context, path), path);
}
@Override
public InputStream asStream() {
return context.getResourceAsStream(path);
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Classes which describe and provide access to arbitrary resources, such as
* the contents of the classpath of a classloader, or files within the web
* application itself.
*/
package org.glyptodon.guacamole.net.basic.resource;