diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/AbstractResource.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/AbstractResource.java new file mode 100644 index 000000000..0e0e9f169 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/AbstractResource.java @@ -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; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ClassPathResource.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ClassPathResource.java new file mode 100644 index 000000000..1f7537835 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ClassPathResource.java @@ -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); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/Resource.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/Resource.java new file mode 100644 index 000000000..2a50b7595 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/Resource.java @@ -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(); + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ResourceServlet.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ResourceServlet.java new file mode 100644 index 000000000..494bebe9e --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ResourceServlet.java @@ -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(); + } + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/SequenceResource.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/SequenceResource.java new file mode 100644 index 000000000..4f42fe7fe --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/SequenceResource.java @@ -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 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 resources) { + + // If no resources, just assume application/octet-stream + Iterator 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 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 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() { + + /** + * Iterator over all resources associated with this + * SequenceResource. + */ + private final Iterator resourceIterator = resources.iterator(); + + @Override + public boolean hasMoreElements() { + return resourceIterator.hasNext(); + } + + @Override + public InputStream nextElement() { + return resourceIterator.next().asStream(); + } + + }); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/WebApplicationResource.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/WebApplicationResource.java new file mode 100644 index 000000000..ad99181fc --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/WebApplicationResource.java @@ -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); + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/package-info.java new file mode 100644 index 000000000..ff6b304cf --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/package-info.java @@ -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;