All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.wicket.request.resource.AbstractResource Maven / Gradle / Ivy

Go to download

Pax Wicket Service is an OSGi extension of the Wicket framework, allowing for dynamic loading and unloading of Wicket components and pageSources.

There is a newer version: 5.0.0
Show newest version
/*
 * 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.wicket.request.resource;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;

import javax.servlet.http.HttpServletResponse;

import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.request.HttpHeaderCollection;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.resource.caching.IResourceCachingStrategy;
import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
import org.apache.wicket.settings.IResourceSettings;
import org.apache.wicket.util.io.Streams;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.time.Duration;
import org.apache.wicket.util.time.Time;

/**
 * Convenience resource implementation. The subclass must implement
 * {@link #newResourceResponse(org.apache.wicket.request.resource.IResource.Attributes)} method.
 * 
 * @author Matej Knopp
 */
public abstract class AbstractResource implements IResource
{
	private static final long serialVersionUID = 1L;

	/** header values that are managed internally and must not be set directly */
	public static final Set INTERNAL_HEADERS;

	static
	{
		INTERNAL_HEADERS = new HashSet();
		INTERNAL_HEADERS.add("server");
		INTERNAL_HEADERS.add("date");
		INTERNAL_HEADERS.add("expires");
		INTERNAL_HEADERS.add("last-modified");
		INTERNAL_HEADERS.add("content-type");
		INTERNAL_HEADERS.add("content-length");
		INTERNAL_HEADERS.add("content-disposition");
		INTERNAL_HEADERS.add("transfer-encoding");
		INTERNAL_HEADERS.add("connection");
		INTERNAL_HEADERS.add("content-disposition");
	}

	/**
	 * Construct.
	 */
	public AbstractResource()
	{
	}

	/**
	 * Override this method to return a {@link ResourceResponse} for the request.
	 * 
	 * @param attributes
	 *            request attributes
	 * @return resource data instance
	 */
	protected abstract ResourceResponse newResourceResponse(Attributes attributes);

	/**
	 * Represents data used to configure response and write resource data.
	 * 
	 * @author Matej Knopp
	 */
	public static class ResourceResponse
	{
		private Integer errorCode;
		private String errorMessage;
		private String fileName = null;
		private ContentDisposition contentDisposition = ContentDisposition.INLINE;
		private String contentType = null;
		private String textEncoding;
		private long contentLength = -1;
		private Time lastModified = null;
		private WriteCallback writeCallback;
		private Duration cacheDuration;
		private WebResponse.CacheScope cacheScope;
		private final HttpHeaderCollection headers;

		/**
		 * Construct.
		 */
		public ResourceResponse()
		{
			// disallow caching for public caches. this behavior is similar to wicket 1.4:
			// setting it to [PUBLIC] seems to be sexy but could potentially cache confidential
			// data on public proxies for users migrating to 1.5
			cacheScope = WebResponse.CacheScope.PRIVATE;

			// collection of directly set response headers
			headers = new HttpHeaderCollection();
		}

		/**
		 * Sets the error code for resource. If there is an error code set the data will not be
		 * rendered and the code will be sent to client.
		 * 
		 * @param errorCode
		 *            error code
		 */
		public void setError(Integer errorCode)
		{
			setError(errorCode, null);
		}

		/**
		 * Sets the error code and message for resource. If there is an error code set the data will
		 * not be rendered and the code and message will be sent to client.
		 * 
		 * @param errorCode
		 *            error code
		 * @param errorMessage
		 *            error message
		 */
		public void setError(Integer errorCode, String errorMessage)
		{
			this.errorCode = errorCode;
			this.errorMessage = errorMessage;
		}

		/**
		 * @return error code or null
		 */
		public Integer getErrorCode()
		{
			return errorCode;
		}

		/**
		 * @return error message or null
		 */
		public String getErrorMessage()
		{
			return errorMessage;
		}

		/**
		 * Sets the file name of the resource.
		 * 
		 * @param fileName
		 *            file name
		 */
		public void setFileName(String fileName)
		{
			this.fileName = fileName;
		}

		/**
		 * @return resource file name
		 */
		public String getFileName()
		{
			return fileName;
		}

		/**
		 * Determines whether the resource will be inline or an attachment.
		 * 
		 * @see ContentDisposition
		 * 
		 * @param contentDisposition
		 *            content disposition (attachment or inline)
		 */
		public void setContentDisposition(ContentDisposition contentDisposition)
		{
			Args.notNull(contentDisposition, "contentDisposition");
			this.contentDisposition = contentDisposition;
		}

		/**
		 * @return whether the resource is inline or attachment
		 */
		public ContentDisposition getContentDisposition()
		{
			return contentDisposition;
		}

		/**
		 * Sets the content type for the resource. If no content type is set it will be determined
		 * by the extension.
		 * 
		 * @param contentType
		 *            content type (also known as mime type)
		 */
		public void setContentType(String contentType)
		{
			this.contentType = contentType;
		}

		/**
		 * @return resource content type
		 */
		public String getContentType()
		{
			if (contentType == null && fileName != null)
			{
				contentType = Application.get().getMimeType(fileName);
			}
			return contentType;
		}

		/**
		 * Sets the text encoding for the resource. This setting must only used 
		 * if the resource response represents text.
		 * 
		 * @param textEncoding
		 *            character encoding of text body
		 */
		public void setTextEncoding(String textEncoding)
		{
			this.textEncoding = textEncoding;
		}

		/**
		 * @return text encoding for resource
		 */
		protected String getTextEncoding()
		{
			return textEncoding;
		}

		/**
		 * Sets the content length (in bytes) of the data. Content length is optional but it's
		 * recommended to set it so that the browser can show download progress.
		 * 
		 * @param contentLength
		 *            length of response body
		 */
		public void setContentLength(long contentLength)
		{
			this.contentLength = contentLength;
		}

		/**
		 * @return content length (in bytes)
		 */
		public long getContentLength()
		{
			return contentLength;
		}

		/**
		 * Sets the last modified data of the resource. Even though this method is optional it is
		 * recommended to set the date. If the date is set properly Wicket can check the
		 * If-Modified-Since to determine if the actuall data really needs to be sent
		 * to client.
		 * 
		 * @param lastModified
		 *            last modification timestamp
		 */
		public void setLastModified(Time lastModified)
		{
			this.lastModified = lastModified;
		}

		/**
		 * @return last modification timestamp
		 */
		public Time getLastModified()
		{
			return lastModified;
		}

		/**
		 * Check to determine if the resource data needs to be written. This method checks the
		 * If-Modified-Since request header and compares it to lastModified property.
		 * In order for this method to work {@link #setLastModified(Time)} has to be called first.
		 * 
		 * @param attributes
		 *            request attributes
		 * @return true if the resource data does need to be written,
		 *         false otherwise.
		 */
		public boolean dataNeedsToBeWritten(Attributes attributes)
		{
			WebRequest request = (WebRequest)attributes.getRequest();
			Time ifModifiedSince = request.getIfModifiedSinceHeader();

			if (cacheDuration != Duration.NONE && ifModifiedSince != null && lastModified != null)
			{
				// [Last-Modified] headers have a maximum precision of one second
				// so we have to truncate the milliseconds part for a proper compare.
				// that's stupid, since changes within one second will not be reliably
				// detected by the client ... any hint or clarification to improve this
				// situation will be appreciated...
				Time roundedLastModified = Time.millis(lastModified.getMilliseconds() / 1000 * 1000);

				return ifModifiedSince.before(roundedLastModified);
			}
			else
			{
				return true;
			}
		}

		/**
		 * disable caching
		 */
		public void disableCaching()
		{
			setCacheDuration(Duration.NONE);
		}

		/**
		 * set caching to maximum available duration
		 */
		public void setCacheDurationToMaximum()
		{
			cacheDuration = WebResponse.MAX_CACHE_DURATION;
		}

		/**
		 * Controls how long this response may be cached
		 * 
		 * @param duration
		 *            caching duration in seconds
		 */
		public void setCacheDuration(Duration duration)
		{
			cacheDuration = Args.notNull(duration, "duration");
		}

		/**
		 * returns how long this resource may be cached
		 * 

* The special value Duration.NONE means caching is disabled. * * @return duration for caching * * @see IResourceSettings#setDefaultCacheDuration(org.apache.wicket.util.time.Duration) * @see IResourceSettings#getDefaultCacheDuration() */ public Duration getCacheDuration() { Duration duration = cacheDuration; if (duration == null && Application.exists()) { duration = Application.get().getResourceSettings().getDefaultCacheDuration(); } return duration; } /** * returns what kind of caches are allowed to cache the resource response *

* resources are only cached at all if caching is enabled by setting a cache duration. * * @return cache scope * * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration() * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(org.apache.wicket.util.time.Duration) * @see org.apache.wicket.request.http.WebResponse.CacheScope */ public WebResponse.CacheScope getCacheScope() { return cacheScope; } /** * controls what kind of caches are allowed to cache the response *

* resources are only cached at all if caching is enabled by setting a cache duration. * * @param scope * scope for caching * * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#getCacheDuration() * @see org.apache.wicket.request.resource.AbstractResource.ResourceResponse#setCacheDuration(org.apache.wicket.util.time.Duration) * @see org.apache.wicket.request.http.WebResponse.CacheScope */ public void setCacheScope(WebResponse.CacheScope scope) { cacheScope = Args.notNull(scope, "scope"); } /** * Sets the {@link WriteCallback}. The callback is responsible for generating the response * data. *

* It is necessary to set the {@link WriteCallback} if * {@link #dataNeedsToBeWritten(org.apache.wicket.request.resource.IResource.Attributes)} * returns true and {@link #setError(Integer)} has not been called. * * @param writeCallback * write callback */ public void setWriteCallback(final WriteCallback writeCallback) { Args.notNull(writeCallback, "writeCallback"); this.writeCallback = writeCallback; } /** * @return write callback. */ public WriteCallback getWriteCallback() { return writeCallback; } /** * get custom headers * * @return collection of the response headers */ public HttpHeaderCollection getHeaders() { return headers; } } /** * Configure the web response header for client cache control. * * @param data * resource data * @param attributes * request attributes */ protected void configureCache(final ResourceResponse data, final Attributes attributes) { Response response = attributes.getResponse(); if (response instanceof WebResponse) { Duration duration = data.getCacheDuration(); WebResponse webResponse = (WebResponse)response; if (duration.compareTo(Duration.NONE) > 0) { webResponse.enableCaching(duration, data.getCacheScope()); } else { webResponse.disableCaching(); } } } protected IResourceCachingStrategy getCachingStrategy() { return Application.get().getResourceSettings().getCachingStrategy(); } /** * * @see org.apache.wicket.request.resource.IResource#respond(org.apache.wicket.request.resource.IResource.Attributes) */ public void respond(final Attributes attributes) { // Get a "new" ResourceResponse to write a response ResourceResponse data = newResourceResponse(attributes); // let caching strategy decorate response if resource is intended to be cached if (this instanceof IStaticCacheableResource) { getCachingStrategy().decorateResponse(data, (IStaticCacheableResource)this); } // set response header setResponseHeaders(data, attributes); if (!data.dataNeedsToBeWritten(attributes) || data.getErrorCode() != null) { return; } if (data.getWriteCallback() == null) { throw new IllegalStateException("ResourceResponse#setWriteCallback() must be set."); } data.getWriteCallback().writeData(attributes); } /** * check if header is directly modifyable * * @param name * header name * * @throws IllegalArgumentException * if access is forbidden */ private void checkHeaderAccess(String name) { name = Args.notEmpty(name.trim().toLowerCase(), "name"); if (INTERNAL_HEADERS.contains(name)) { throw new IllegalArgumentException("you are not allowed to directly access header [" + name + "], " + "use one of the other specialized methods of " + getClass().getSimpleName() + " to get or modify its value"); } } /** * @param data * @param attributes */ protected void setResponseHeaders(final ResourceResponse data, final Attributes attributes) { Response response = attributes.getResponse(); if (response instanceof WebResponse) { WebResponse webResponse = (WebResponse)response; // 1. Last Modified Time lastModified = data.getLastModified(); if (lastModified != null) { webResponse.setLastModifiedTime(lastModified); } // 2. Caching configureCache(data, attributes); if (!data.dataNeedsToBeWritten(attributes)) { webResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } if (data.getErrorCode() != null) { webResponse.sendError(data.getErrorCode(), data.getErrorMessage()); return; } String fileName = data.getFileName(); ContentDisposition disposition = data.getContentDisposition(); String mimeType = data.getContentType(); long contentLength = data.getContentLength(); // 3. Content Disposition if (ContentDisposition.ATTACHMENT == disposition) { webResponse.setAttachmentHeader(fileName); } else if (ContentDisposition.INLINE == disposition) { webResponse.setInlineHeader(fileName); } // 4. Mime Type (+ encoding) if (mimeType != null) { final String encoding = data.getTextEncoding(); if (encoding == null) { webResponse.setContentType(mimeType); } else { webResponse.setContentType(mimeType + "; charset=" + encoding); } } // 5. Content Length if (contentLength != -1) { webResponse.setContentLength(contentLength); } // add custom headers and values final HttpHeaderCollection headers = data.getHeaders(); for (String name : headers.getHeaderNames()) { checkHeaderAccess(name); for (String value : headers.getHeaderValues(name)) { webResponse.addHeader(name, value); } } // 6. Flush the response flushResponseAfterHeaders(webResponse); } } /** * Flushes the response after setting the headers. * This is necessary for Firefox if this resource is an image, * otherwise it messes up other images on page. * * @param response * the current web response */ protected void flushResponseAfterHeaders(final WebResponse response) { response.flush(); } /** * Callback invoked when resource data needs to be written to response. Subclass needs to * implement the {@link #writeData(org.apache.wicket.request.resource.IResource.Attributes)} * method. * * @author Matej Knopp */ public abstract static class WriteCallback { /** * Write the resource data to response. * * @param attributes * request attributes */ public abstract void writeData(Attributes attributes); /** * Convenience method to write an {@link InputStream} to response. * * @param attributes * request attributes * @param stream * input stream */ protected final void writeStream(Attributes attributes, InputStream stream) { final Response response = attributes.getResponse(); OutputStream s = new OutputStream() { @Override public void write(int b) throws IOException { response.write(new byte[] { (byte)b }); } @Override public void write(byte[] b) throws IOException { response.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { response.write(b, off, len); } }; try { Streams.copy(stream, s); } catch (IOException e) { throw new WicketRuntimeException(e); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy