package ro.isdc.wro.model.resource.processor.support;
import static org.apache.commons.lang3.Validate.notNull;
import static ro.isdc.wro.util.StringUtils.cleanPath;
import static ro.isdc.wro.util.WroUtil.cleanImageUrl;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.model.resource.locator.ClasspathUriLocator;
import ro.isdc.wro.model.resource.locator.ServletContextUriLocator;
import ro.isdc.wro.model.resource.locator.UrlUriLocator;
import ro.isdc.wro.util.WroUtil;
/**
* Responsible for computing the url of the images from css based on the location of the css where they are located.
*
* @author Alex Objelean
* @since 1.7.0
*/
public class ImageUrlRewriter {
private static final Logger LOG = LoggerFactory.getLogger(ImageUrlRewriter.class);
private static final String ROOT_CONTEXT_PATH = ServletContextUriLocator.PREFIX;
private static final String FOLDER_PREFIX = "/..";
private final RewriterContext context;
/**
* Holds the properties required by this class to perform rewrite operation.
*/
public static final class RewriterContext {
private String proxyPrefix;
private String aggregatedFolderPath;
private String contextPath;
public RewriterContext setProxyPrefix(final String proxyPrefix) {
this.proxyPrefix = proxyPrefix;
return this;
}
public RewriterContext setAggregatedFolderPath(final String aggregatedFolderPath) {
this.aggregatedFolderPath = aggregatedFolderPath;
return this;
}
public RewriterContext setContextPath(final String contextPath) {
this.contextPath = contextPath;
return this;
}
}
public ImageUrlRewriter(final RewriterContext context) {
notNull(context);
notNull(context.proxyPrefix);
if (context.contextPath == null) {
context.setContextPath(ROOT_CONTEXT_PATH);
}
this.context = context;
}
/**
* Computes the url of the image to be replaced in a css resource.
*
* @param cssUri
* the uri of the css where the image is located.
* @param imageUrl
* the url of the image (relative or absolute).
* @return replaced url of the image.
*/
public String rewrite(final String cssUri, final String imageUrl) {
notNull(cssUri);
notNull(imageUrl);
if (StringUtils.isEmpty(imageUrl)) {
return imageUrl;
}
if (ServletContextUriLocator.isValid(cssUri)) {
if (ServletContextUriLocator.isValid(imageUrl)) {
return prependContextPath(imageUrl);
}
// Treat WEB-INF special case
if (ServletContextUriLocator.isProtectedResource(cssUri)) {
return context.proxyPrefix + computeNewImageLocation(cssUri, imageUrl);
}
// Compute the folder where the final css is located. This is important for computing image location after url
// rewriting.
// Prefix of the path to the overwritten image url. This will be of the following type: "../" or "../.." depending
// on the depth of the aggregatedFolderPath.
final String aggregatedPathPrefix = computeAggregationPathPrefix(context.aggregatedFolderPath);
LOG.debug("computed aggregatedPathPrefix {}", aggregatedPathPrefix);
String newImageLocation = computeNewImageLocation(aggregatedPathPrefix + cssUri, imageUrl);
if (newImageLocation.startsWith(ServletContextUriLocator.PREFIX)) {
newImageLocation = prependContextPath(newImageLocation);
}
LOG.debug("newImageLocation: {}", newImageLocation);
return newImageLocation;
}
if (UrlUriLocator.isValid(cssUri)) {
final String computedCssUri = ServletContextUriLocator.isValid(imageUrl) ? computeCssUriForExternalServer(cssUri)
: cssUri;
return computeNewImageLocation(computedCssUri, imageUrl);
}
if (ClasspathUriLocator.isValid(cssUri)) {
final String proxyUrl = context.proxyPrefix + computeNewImageLocation(cssUri, imageUrl);
final String contextRelativeUrl = prependContextPath(imageUrl);
//final String contextRelativeUrl = context.contextPath + imageUrl;
// leave imageUrl unchanged if it is a servlet context relative resource
return (ServletContextUriLocator.isValid(imageUrl) ? contextRelativeUrl : proxyUrl);
}
throw new WroRuntimeException("Could not replace imageUrl: " + imageUrl + ", contained at location: " + cssUri);
}
private String prependContextPath(final String imageUrl) {
//avoid double slash
final String contextRelativeUrl = context.contextPath.endsWith(ROOT_CONTEXT_PATH) ? imageUrl
: context.contextPath + imageUrl;
return contextRelativeUrl;
}
/**
* @return the path to be prefixed after css aggregation. This depends on the aggregated css destination folder. This
* is a fix for the following issue: {@link http://code.google.com/p/wro4j/issues/detail?id=259}
*/
private String computeAggregationPathPrefix(final String aggregatedFolderPath) {
LOG.debug("aggregatedFolderPath: {}", aggregatedFolderPath);
String computedPrefix = StringUtils.EMPTY;
if (aggregatedFolderPath != null) {
final StringBuffer result = new StringBuffer("");
final String[] depthFolders = WroUtil.normalize(aggregatedFolderPath).split(ROOT_CONTEXT_PATH);
LOG.debug("subfolders {}", Arrays.toString(depthFolders));
for (final String folder : depthFolders) {
if (!StringUtils.isEmpty(folder)) {
result.append(FOLDER_PREFIX);
}
}
computedPrefix = result.toString().replaceFirst(ROOT_CONTEXT_PATH, "");
}
LOG.debug("computedPrefix: {}", computedPrefix);
return computedPrefix;
}
/**
* Css files hosted on external server, should use its host as the root context when rewriting image url's starting
* with '/' character.
*/
private String computeCssUriForExternalServer(final String cssUri) {
String exernalServerCssUri = cssUri;
try {
// compute the host of the external server (with protocol & port).
final String serverHost = cssUri.replace(new URL(cssUri).getPath(), "");
// the uri should end mandatory with /
exernalServerCssUri = serverHost + ServletContextUriLocator.PREFIX;
LOG.debug("using {} host as cssUri", exernalServerCssUri);
} catch (final MalformedURLException e) {
// should never happen
}
return exernalServerCssUri;
}
/**
* Concatenates cssUri and imageUrl after few changes are applied to both input parameters.
*
* @param cssUri
* the URI of css resource.
* @param imageUrl
* the URL of image referred in css.
* @return processed new location of image url.
*/
private String computeNewImageLocation(final String cssUri, final String imageUrl) {
LOG.debug("cssUri: {}, imageUrl {}", cssUri, imageUrl);
final String cleanImageUrl = cleanImageUrl(imageUrl);
// TODO move to ServletContextUriLocator as a helper method?
// for the following input: /a/b/c/1.css => /a/b/c/
int idxLastSeparator = cssUri.lastIndexOf(ServletContextUriLocator.PREFIX);
if (idxLastSeparator == -1) {
if (ClasspathUriLocator.isValid(cssUri)) {
idxLastSeparator = cssUri.lastIndexOf(ClasspathUriLocator.PREFIX);
// find the index of ':' character used by classpath prefix
if (idxLastSeparator >= 0) {
idxLastSeparator += ClasspathUriLocator.PREFIX.length() - 1;
}
}
if (idxLastSeparator < 0) {
throw new IllegalStateException("Invalid cssUri: " + cssUri + ". Should contain at least one '/' character!");
}
}
final String cssUriFolder = cssUri.substring(0, idxLastSeparator + 1);
// remove '/' from imageUrl if it starts with one.
final String processedImageUrl = cleanImageUrl.startsWith(ServletContextUriLocator.PREFIX) ? cleanImageUrl.substring(1)
: cleanImageUrl;
final String computedImageLocation = cleanPath(cssUriFolder + processedImageUrl);
LOG.debug("computedImageLocation: {}", computedImageLocation);
return computedImageLocation;
}
}