/*
* 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.Serializable;
import java.util.Locale;
import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.Application;
import org.apache.wicket.Session;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.markup.html.IPackageResourceGuard;
import org.apache.wicket.request.resource.caching.IStaticCacheableResource;
import org.apache.wicket.settings.IResourceSettings;
import org.apache.wicket.util.io.IOUtils;
import org.apache.wicket.util.lang.Packages;
import org.apache.wicket.core.util.lang.WicketObjects;
import org.apache.wicket.util.resource.IFixedLocationResourceStream;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.apache.wicket.core.util.resource.locator.IResourceStreamLocator;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a localizable static resource.
* <p>
* Use like eg:
*
* <pre>
* MyPackageResource IMG_UNKNOWN = new MyPackageResource(EditPage.class, "questionmark.gif");
* </pre>
*
* where the static resource references image 'questionmark.gif' from the the package that EditPage
* is in to get a package resource.
* </p>
*
* Access to resources can be granted or denied via a {@link IPackageResourceGuard}. Please see
* {@link IResourceSettings#getPackageResourceGuard()} as well.
*
* @author Jonathan Locke
* @author Eelco Hillenius
* @author Juergen Donnerstag
* @author Matej Knopp
*/
public class PackageResource extends AbstractResource implements IStaticCacheableResource
{
private static final Logger log = LoggerFactory.getLogger(PackageResource.class);
private static final long serialVersionUID = 1L;
/**
* Exception thrown when the creation of a package resource is not allowed.
*/
public static final class PackageResourceBlockedException extends WicketRuntimeException
{
private static final long serialVersionUID = 1L;
/**
* Construct.
*
* @param message
* error message
*/
public PackageResourceBlockedException(String message)
{
super(message);
}
}
/**
* The path to the resource
*/
private final String absolutePath;
/**
* The resource's locale
*/
private final Locale locale;
/**
* The path this resource was created with.
*/
private final String path;
/**
* The scoping class, used for class loading and to determine the package.
*/
private final String scopeName;
/**
* The resource's style
*/
private final String style;
/**
* The component's variation (of the style)
*/
private final String variation;
/**
* A flag indicating whether {@code ITextResourceCompressor} can be used to compress this resource.
* Default is {@code false} because this resource may be used for binary data (e.g. an image).
* Specializations of this class should change this flag appropriately.
*/
private boolean compress = false;
/**
* controls whether {@link org.apache.wicket.request.resource.caching.IResourceCachingStrategy}
* should be applied to resource
*/
private boolean cachingEnabled = true;
/**
* text encoding (may be null) - only makes sense for character-based resources
*/
private String textEncoding = null;
/**
* Hidden constructor.
*
* @param scope
* This argument will be used to get the class loader for loading the package
* resource, and to determine what package it is in
* @param name
* The relative path to the resource
* @param locale
* The locale of the resource
* @param style
* The style of the resource
* @param variation
* The component's variation (of the style)
*/
protected PackageResource(final Class<?> scope, final String name, final Locale locale,
final String style, final String variation)
{
// Convert resource path to absolute path relative to base package
absolutePath = Packages.absolutePath(scope, name);
final String parentEscape = getParentFolderPlaceholder();
if (Strings.isEmpty(parentEscape) == false)
{
path = Strings.replaceAll(name, "../", parentEscape + "/").toString();
}
else
{
path = name;
}
this.scopeName = scope.getName();
this.locale = locale;
this.style = style;
this.variation = variation;
}
private Locale getCurrentLocale()
{
return locale != null ? locale : Session.get().getLocale();
}
private String getCurrentStyle()
{
return style != null ? style : Session.get().getStyle();
}
@Override
public boolean isCachingEnabled()
{
return cachingEnabled;
}
public void setCachingEnabled(final boolean enabled)
{
this.cachingEnabled = enabled;
}
/**
* get text encoding (intented for character-based resources)
* @return custom encoding or {@code null} to use default
*/
public String getTextEncoding()
{
return textEncoding;
}
/**
* set text encoding (intented for character-based resources)
*
* @param textEncoding
* custom encoding or {@code null} to use default
*/
public void setTextEncoding(final String textEncoding)
{
this.textEncoding = textEncoding;
}
@Override
public Serializable getCacheKey()
{
IResourceStream stream = getCacheableResourceStream();
// if resource stream can not be found do not cache
if (stream == null)
{
return null;
}
return new CacheKey(scopeName, absolutePath, stream.getLocale(), stream.getStyle(),
stream.getVariation());
}
/**
* Gets the scoping class, used for class loading and to determine the package.
*
* @return the scoping class
*/
public final Class<?> getScope()
{
return WicketObjects.resolveClass(scopeName);
}
/**
* Gets the style.
*
* @return the style
*/
public final String getStyle()
{
return style;
}
/**
* creates a new resource response based on the request attributes
*
* @param attributes
* current request attributes from client
* @return resource response for answering request
*/
@Override
protected ResourceResponse newResourceResponse(Attributes attributes)
{
final ResourceResponse resourceResponse = new ResourceResponse();
final IResourceStream resourceStream = getResourceStream();
// bail out if resource stream could not be found
if (resourceStream == null)
{
return sendResourceError(resourceResponse, HttpServletResponse.SC_NOT_FOUND,
"Unable to find resource");
}
// add Last-Modified header (to support HEAD requests and If-Modified-Since)
final Time lastModified = resourceStream.lastModifiedTime();
resourceResponse.setLastModified(lastModified);
if (resourceResponse.dataNeedsToBeWritten(attributes))
{
String contentType = resourceStream.getContentType();
if (contentType == null && Application.exists())
{
contentType = Application.get().getMimeType(path);
}
// set Content-Type (may be null)
resourceResponse.setContentType(contentType);
// set content encoding (may be null)
resourceResponse.setTextEncoding(getTextEncoding());
try
{
// read resource data
final byte[] bytes;
bytes = IOUtils.toByteArray(resourceStream.getInputStream());
final byte[] processed = processResponse(attributes, bytes);
// send Content-Length header
resourceResponse.setContentLength(processed.length);
// send response body with resource data
resourceResponse.setWriteCallback(new WriteCallback()
{
@Override
public void writeData(Attributes attributes)
{
attributes.getResponse().write(processed);
}
});
}
catch (IOException e)
{
log.debug(e.getMessage(), e);
return sendResourceError(resourceResponse, 500, "Unable to read resource stream");
}
catch (ResourceStreamNotFoundException e)
{
log.debug(e.getMessage(), e);
return sendResourceError(resourceResponse, 500, "Unable to open resource stream");
}
finally
{
try
{
resourceStream.close();
}
catch (IOException e)
{
log.warn("Unable to close the resource stream", e);
}
}
}
return resourceResponse;
}
/**
* Gives a chance to modify the resource going to be written in the response
*
* @param attributes
* current request attributes from client
* @param original
* the original response
* @return the processed response
*/
protected byte[] processResponse(final Attributes attributes, final byte[] original)
{
return original;
}
/**
* send resource specific error message and write log entry
*
* @param resourceResponse
* resource response
* @param errorCode
* error code (=http status)
* @param errorMessage
* error message (=http error message)
* @return resource response for method chaining
*/
private ResourceResponse sendResourceError(ResourceResponse resourceResponse, int errorCode,
String errorMessage)
{
String msg = String.format(
"resource [path = %s, style = %s, variation = %s, locale = %s]: %s (status=%d)",
absolutePath, style, variation, locale, errorMessage, errorCode);
log.warn(msg);
resourceResponse.setError(errorCode, errorMessage);
return resourceResponse;
}
/**
* be aware that method takes the current wicket session's locale and style into account when
* locating the stream.
*
* @return resource stream
*
* @see org.apache.wicket.request.resource.caching.IStaticCacheableResource#getCacheableResourceStream()
* @see #getResourceStream()
*/
@Override
public IResourceStream getCacheableResourceStream()
{
return internalGetResourceStream(getCurrentStyle(), getCurrentLocale());
}
/**
* locate resource stream for current resource
*
* @return resource stream or <code>null</code> if not found
*/
public IResourceStream getResourceStream()
{
return internalGetResourceStream(style, locale);
}
/**
* @return whether {@link org.apache.wicket.resource.ITextResourceCompressor} can be used to compress the
* resource.
*/
public boolean getCompress()
{
return compress;
}
/**
* @param compress
* A flag indicating whether the resource should be compressed.
*/
public void setCompress(boolean compress)
{
this.compress = compress;
}
private IResourceStream internalGetResourceStream(final String style, final Locale locale)
{
IResourceStreamLocator resourceStreamLocator = Application.get()
.getResourceSettings()
.getResourceStreamLocator();
IResourceStream resourceStream = resourceStreamLocator.locate(getScope(), absolutePath, style, variation, locale, null, false);
Class<?> realScope = getScope();
String realPath = absolutePath;
if (resourceStream instanceof IFixedLocationResourceStream)
{
realPath = ((IFixedLocationResourceStream)resourceStream).locationAsString();
if (realPath != null)
{
int index = realPath.indexOf(absolutePath);
if (index != -1)
{
realPath = realPath.substring(index);
}
else
{
// just fall back on the full path without a scope..
realScope = null;
}
}
else
{
realPath = absolutePath;
}
}
if (accept(realScope, realPath) == false)
{
throw new PackageResourceBlockedException(
"Access denied to (static) package resource " + absolutePath +
". See IPackageResourceGuard");
}
return resourceStream;
}
/**
* @param scope
* resource scope
* @param path
* resource path
* @return <code>true<code> if resource access is granted
*/
private boolean accept(Class<?> scope, String path)
{
IPackageResourceGuard guard = Application.get()
.getResourceSettings()
.getPackageResourceGuard();
return guard.accept(scope, path);
}
/**
* Gets whether a resource for a given set of criteria exists.
*
* @param scope
* This argument will be used to get the class loader for loading the package
* resource, and to determine what package it is in. Typically this is the class in
* which you call this method
* @param path
* The path to the resource
* @param locale
* The locale of the resource
* @param style
* The style of the resource (see {@link org.apache.wicket.Session})
* @param variation
* The component's variation (of the style)
* @return true if a resource could be loaded, false otherwise
*/
public static boolean exists(final Class<?> scope, final String path, final Locale locale,
final String style, final String variation)
{
String absolutePath = Packages.absolutePath(scope, path);
return Application.get()
.getResourceSettings()
.getResourceStreamLocator()
.locate(scope, absolutePath, style, variation, locale, null, false) != null;
}
@Override
public String toString()
{
final StringBuilder result = new StringBuilder();
result.append('[')
.append(getClass().getSimpleName())
.append(' ')
.append("name = ")
.append(path)
.append(", scope = ")
.append(scopeName)
.append(", locale = ")
.append(locale)
.append(", style = ")
.append(style)
.append(", variation = ")
.append(variation)
.append(']');
return result.toString();
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((absolutePath == null) ? 0 : absolutePath.hashCode());
result = prime * result + ((locale == null) ? 0 : locale.hashCode());
result = prime * result + ((path == null) ? 0 : path.hashCode());
result = prime * result + ((scopeName == null) ? 0 : scopeName.hashCode());
result = prime * result + ((style == null) ? 0 : style.hashCode());
result = prime * result + ((variation == null) ? 0 : variation.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PackageResource other = (PackageResource)obj;
if (absolutePath == null)
{
if (other.absolutePath != null)
return false;
}
else if (!absolutePath.equals(other.absolutePath))
return false;
if (locale == null)
{
if (other.locale != null)
return false;
}
else if (!locale.equals(other.locale))
return false;
if (path == null)
{
if (other.path != null)
return false;
}
else if (!path.equals(other.path))
return false;
if (scopeName == null)
{
if (other.scopeName != null)
return false;
}
else if (!scopeName.equals(other.scopeName))
return false;
if (style == null)
{
if (other.style != null)
return false;
}
else if (!style.equals(other.style))
return false;
if (variation == null)
{
if (other.variation != null)
return false;
}
else if (!variation.equals(other.variation))
return false;
return true;
}
String getParentFolderPlaceholder()
{
String parentFolderPlaceholder;
if (Application.exists())
{
parentFolderPlaceholder = Application.get()
.getResourceSettings()
.getParentFolderPlaceholder();
}
else
{
parentFolderPlaceholder = "..";
}
return parentFolderPlaceholder;
}
private static class CacheKey implements Serializable
{
private final String scopeName;
private final String path;
private final Locale locale;
private final String style;
private final String variation;
public CacheKey(String scopeName, String path, Locale locale, String style, String variation)
{
this.scopeName = scopeName;
this.path = path;
this.locale = locale;
this.style = style;
this.variation = variation;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof CacheKey))
return false;
CacheKey cacheKey = (CacheKey)o;
if (locale != null ? !locale.equals(cacheKey.locale) : cacheKey.locale != null)
return false;
if (!path.equals(cacheKey.path))
return false;
if (!scopeName.equals(cacheKey.scopeName))
return false;
if (style != null ? !style.equals(cacheKey.style) : cacheKey.style != null)
return false;
if (variation != null ? !variation.equals(cacheKey.variation)
: cacheKey.variation != null)
return false;
return true;
}
@Override
public int hashCode()
{
int result = scopeName.hashCode();
result = 31 * result + path.hashCode();
result = 31 * result + (locale != null ? locale.hashCode() : 0);
result = 31 * result + (style != null ? style.hashCode() : 0);
result = 31 * result + (variation != null ? variation.hashCode() : 0);
return result;
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder();
sb.append("CacheKey");
sb.append("{scopeName='").append(scopeName).append('\'');
sb.append(", path='").append(path).append('\'');
sb.append(", locale=").append(locale);
sb.append(", style='").append(style).append('\'');
sb.append(", variation='").append(variation).append('\'');
sb.append('}');
return sb.toString();
}
}
}