/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.content;
import ch.entwine.weblounge.common.language.Language;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
/**
* This class contains utility methods intended to facilitate dealing with
* resource versions and names.
*/
public final class ResourceUtils {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(ResourceUtils.class);
/** File size units */
private static final String[] SIZE_UNITS = { "B", "kB", "MB", "GB", "TB" };
/**
* This class is not intended to be instantiated.
*/
private ResourceUtils() {
// Nothing to do here
}
/**
* Returns <code>true</code> if the two uris match either by id or path. This
* implementation takes into account that certain fields such as the id may
* not (yet) be set.
*
* @param a
* the first uri
* @param b
* the second uri
* @return <code>true</code> if the two uris point at the same resource
*/
public static boolean equalsByIdOrPathAndVersion(ResourceURI a, ResourceURI b) {
return uriEquals(a, b, true);
}
/**
* Returns <code>true</code> if the two uris match either by id or path and
* version. This implementation takes into account that certain fields such as
* the id may not (yet) be set.
*
* @param a
* the first uri
* @param b
* the second uri
* @return <code>true</code> if the two uris point at the same resource
*/
public static boolean equalsByIdOrPath(ResourceURI a, ResourceURI b) {
return uriEquals(a, b, false);
}
/**
* Returns <code>true</code> if the two uris match. This implementation takes
* into account that certain fields such as the id may not (yet) be set.
*
* @param a
* the first uri
* @param b
* the second uri
* @param checkVersions
* <code>true</code> to also check equality on the version
* @return <code>true</code> if the two uris point at the same resource
*/
private static boolean uriEquals(ResourceURI a, ResourceURI b,
boolean checkVersions) {
long versionA = a.getVersion();
long versionB = b.getVersion();
// Test the identifier
String idA = a.getIdentifier();
String idB = b.getIdentifier();
if (idA != null && idB != null) {
if (idA.equals(idB) && (!checkVersions || versionA == versionB))
return true;
return false;
}
// Test the path
String pathA = a.getPath();
String pathB = b.getPath();
if (pathA != null && pathB != null) {
if (pathA.equals(pathB) && (!checkVersions || versionA == versionB))
return true;
return false;
}
return false;
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side or the request does not contain caching
* information.
* <p>
* The calculation is made based on the availability of either the
* <code>If-None-Match</code> or the <code>If-Modified-Since</code> header (in
* this order).
*
* @param request
* the client request
* @param resource
* the resource
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean hasChanged(HttpServletRequest request,
Resource<?> resource) throws IllegalArgumentException {
return hasChanged(request, resource, null);
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side or the request does not contain caching
* information.
* <p>
* The calculation is made based on the availability of either the
* <code>If-None-Match</code> or the <code>If-Modified-Since</code> header (in
* this order).
*
* @param request
* the client request
* @param resource
* the resource
* @param language
* the language
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean hasChanged(HttpServletRequest request,
Resource<?> resource, Language language) throws IllegalArgumentException {
if (request.getHeader("If-None-Match") != null) {
return isMismatch(request, getETagValue(resource));
} else if (request.getHeader("If-Modified-Since") != null) {
return isModified(request, resource, language);
}
return true;
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side with respect to the file's modification
* date or the request does not contain caching information.
* <p>
* The calculation is made based on the availability of either the
* <code>If-None-Match</code> or the <code>If-Modified-Since</code> header (in
* this order).
*
* @param request
* the client request
* @param file
* the file
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean hasChanged(HttpServletRequest request, File file)
throws IllegalArgumentException {
if (file == null || !file.isFile())
return true;
return hasChanged(request, file.lastModified());
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side or the request does not contain caching
* information.
* <p>
* The calculation is made based on the availability of either the
* <code>If-None-Match</code> or the <code>If-Modified-Since</code> header (in
* this order).
*
* @param request
* the client request
* @param date
* the date
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean hasChanged(HttpServletRequest request, long date)
throws IllegalArgumentException {
if (request.getHeader("If-None-Match") != null) {
return isMismatch(request, getETagValue(date));
} else if (request.getHeader("If-Modified-Since") != null) {
return isModified(request, date);
}
return true;
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side or the request does not contain caching
* information.
* <p>
* The calculation is made based on the availability of the
* <code>If-Modified-Since</code> header. Use
* {@link #hasChanged(HttpServletRequest, Resource))} to also take the
* <code>If-None-Match</code> header into account.
*
* @param request
* the client request
* @param resource
* the resource
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean isModified(HttpServletRequest request,
Resource<?> resource) throws IllegalArgumentException {
return isModified(request, getModificationDate(resource, null).getTime());
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side or the request does not contain caching
* information.
* <p>
* The calculation is made based on the availability of the
* <code>If-Modified-Since</code> header. Use
* {@link #hasChanged(HttpServletRequest, Resource, Language))} to also take
* the <code>If-None-Match</code> header into account.
*
* @param request
* the client request
* @param resource
* the resource
* @param language
* the language
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean isModified(HttpServletRequest request,
Resource<?> resource, Language language) throws IllegalArgumentException {
return isModified(request, getModificationDate(resource, language).getTime());
}
/**
* Returns <code>true</code> if the resource either is more recent than the
* cached version on the client side or the request does not contain caching
* information.
* <p>
* The calculation is made based on the availability of the
* <code>If-Modified-Since</code> header. Use
* {@link #hasChanged(HttpServletRequest, long)} to also take the
* <code>If-None-Match</code> header into account.
*
* @param request
* the client request
* @param date
* the date
* @return <code>true</code> if the resource is more recent than the version
* that is cached at the client.
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> header cannot be converted
* to a date.
*/
public static boolean isModified(HttpServletRequest request, long date)
throws IllegalArgumentException {
if (request.getHeader("If-Modified-Since") != null) {
try {
long cachedModificationDate = request.getDateHeader("If-Modified-Since");
return cachedModificationDate < date;
} catch (IllegalArgumentException e) {
logger.debug("Client sent malformed 'If-Modified-Since' header: {}");
}
}
return true;
}
/**
* Returns <code>true</code> if the resource has not been modified according
* to the expected <code>ETag</code> value, <code>false</code> if the cached
* version on the client side is out dated or if the request did not contain
* caching information.
* <p>
* The decision is based on the availability and value of the
* <code>If-None-Match</code> header (called <code>ETag</code>). The computed
* value of <code>eTag</code> is expected to be plain, i. e. without
* surrounding quotes.
*
* @param eTag
* the expected eTag value
* @param request
* the client request
* @return <code>true</code> if the resource's calculated eTag matches the one
* specified
* @throws IllegalArgumentException
* if the <code>If-Modified-Since</code> cannot be converted to a
* date.
*/
public static boolean isMismatch(HttpServletRequest request, String eTag)
throws IllegalArgumentException {
String eTagHeader = request.getHeader("If-None-Match");
if (StringUtils.isBlank(eTagHeader))
return true;
return !eTagHeader.equals(eTag);
}
/**
* Returns the value for the <code>ETag</code> header field, which is
* calculated from the resource identifier and the resource's modification
* date.
*
* @param resource
* the resource
* @return the <code>ETag</code> value
*/
public static String getETagValue(Resource<?> resource) {
return getETagValue(getModificationDate(resource, null).getTime());
}
/**
* Returns the value for the <code>ETag</code> header field, which is
* calculated from the file's modification date.
*
* @param file
* the file
* @return the <code>ETag</code> value
* @throws IllegalArgumentException
* if <code>file</code> is <code>null</code>
*/
public static String getETagValue(File file) {
if (file == null)
throw new IllegalArgumentException("File must not be null");
return getETagValue(file.lastModified());
}
/**
* Returns the value for the <code>ETag</code> header field, which is
* calculated from the given modification date.
*
* @param date
* the date in milliseconds
* @return the <code>ETag</code> value
*/
public static String getETagValue(long date) {
return new StringBuffer().append("\"").append("WL-" + date).append("\"").toString();
}
/**
* Returns the modification date. If the resource has never been modified, its
* creation date is returned instead.
*
* @param resource
* the resource
* @return the modification date
* @throws IllegalArgumentException
* if the resource is <code>null</code>
*/
public static Date getModificationDate(Resource<?> resource)
throws IllegalArgumentException {
return getModificationDate(resource, null);
}
/**
* Returns the modification date. If the resource has never been modified, its
* creation date is returned instead.
*
* @param resource
* the resource
* @param language
* the language
* @return the modification date
* @throws IllegalArgumentException
* if the resource is <code>null</code>
*/
public static Date getModificationDate(Resource<?> resource, Language language)
throws IllegalArgumentException {
if (resource == null)
throw new IllegalArgumentException("Resource cannot be null");
// Has a language been specified? If so, use the localized content
if (language != null) {
ResourceContent content = resource.getContent(language);
if (content != null)
return content.getCreationDate();
}
// The resource's modified date is the last resort. If nothing else helps,
// return the creation date.
return resource.getLastModified();
}
/**
* Returns the version for the given version identifier. Available versions
* are:
* <ul>
* <li>{@link Resource#LIVE}</li>
* <li>{@link Resource#WORK}</li>
*
* @param version
* the version identifier
* @return the version string
*/
public static long getVersion(String version) {
if ("live".equals(version) || "index".equals(version)) {
return Resource.LIVE;
} else if ("work".equals(version)) {
return Resource.WORK;
} else {
try {
return Long.parseLong(version);
} catch (NumberFormatException e) {
return -1;
}
}
}
/**
* Returns the document name for the given version. For the live version, this
* method will return <code>index.xml</code>. Available versions are:
* <ul>
* <li>{@link Resource#LIVE}</li>
* <li>{@link Resource#WORK}</li>
*
* @param version
* the version identifier
* @return the version string
*/
public static String getDocument(long version) {
if (version == Resource.LIVE)
return "index.xml";
else if (version == Resource.WORK)
return "work.xml";
else
return Long.toString(version) + ".xml";
}
/**
* Returns the version identifier for the given version. Available versions
* are:
* <ul>
* <li>{@link Resource#LIVE}</li>
* <li>{@link Resource#WORK}</li>
*
* @param version
* the version identifier
* @return the version string
*/
public static String getVersionString(long version) {
if (version == Resource.LIVE)
return "live";
else if (version == Resource.WORK)
return "work";
else
return Long.toString(version);
}
/**
* Returns the file size formatted in <tt>bytes</tt>, <tt>kilobytes</tt>,
* <tt>megabytes</tt>, <tt>gigabytes</tt> and <tt>terabytes</tt>, where 1 KB
* is equal to 1'024 bytes.
* <p>
* The number in front of the size unit will be formatted with one decimal
* place, e. g. <tt>12.4KB</tt>.
*
* @param sizeInBytes
* the file size in bytes
* @return the file size formatted using appropriate units
* @throws IllegalArgumentException
* if the file size is negative
*/
public static String formatFileSize(long sizeInBytes)
throws IllegalArgumentException {
return formatFileSize(sizeInBytes, null);
}
/**
* Returns the file size formatted in <tt>bytes</tt>, <tt>kilobytes</tt>,
* <tt>megabytes</tt>, <tt>gigabytes</tt> and <tt>terabytes</tt>, where 1 kB
* is equal to 1'000 bytes.
* <p>
* If no {@link NumberFormat} was provided,
* <code>new DecimalFormat("0.#)</code> is used.
*
* @param sizeInBytes
* the file size in bytes
* @param format
* the number format
* @return the file size formatted using appropriate units
* @throws IllegalArgumentException
* if the file size is negative
*/
public static String formatFileSize(long sizeInBytes, NumberFormat format)
throws IllegalArgumentException {
if (sizeInBytes < 0)
throw new IllegalArgumentException("File size cannot be negative");
int unitSelector = 0;
double size = sizeInBytes;
// Calculate the size to display
while (size >= 1000 && unitSelector < SIZE_UNITS.length) {
size /= 1000.0d;
unitSelector++;
}
// Create a number formatter, if none was provided
if (format == null) {
DecimalFormatSymbols formatSymbols = new DecimalFormatSymbols();
formatSymbols.setDecimalSeparator('.');
format = new DecimalFormat("0.#", formatSymbols);
}
return format.format(size) + SIZE_UNITS[unitSelector];
}
}