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; } }