A common problem a web developer faces is the way browsers cache css files. If not handled properly the end user could get a rather funny looking web page from time to time depending on how often the web page changes styling.
A way to circumvent this is to rename the web page top most css file each time the styling is changed. In a distributed development team it can easily get cumbersome to coordinate the renaming of the css file so that referring css links not get broken. Another issue with renaming of files in a development team (using any kind of versioning system) creates a lot of noise in the commit log.
If you are using maven as your compile agent there is a nice way to write the build version into the WAR MANIFEST file.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<warName>minvolvo</warName>
<archive>
<manifestEntries>
<Implementation-Version>${project.version}-b${build.number}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
Having the version number in the manifest file you can use that version number to fool the browser that we change the css file name on each build.
Create logic which can read the version number from the manifest file.
public class ManifestBuilderImpl implements ManifestBuilder,
ServletContextAware {
private ServletContext servletContext;
@Override
public Manifest build(String relativePath) throws IOException {
File manifestFile = getRealPath(relativePath);
Manifest manifest = new Manifest();
manifest.read(new FileInputStream(manifestFile));
return manifest;
}
private File getRealPath(String relativePath) {
String realPath = String.format("%s%s", servletContext.getRealPath("/"), relativePath);
File manifestFile = new File(realPath);
return manifestFile;
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}
Create some sort of cache to store the manifest values in a accessible manner for actions.
public class ManifestCacheImpl implements ManifestCache,
ServletContextAware {
private ManifestReader reader;
private ServletContext servletContext;
@Override
public String getManifestValue(String attributeName) {
String key = String.format("MANIFEST-%s", attributeName);
String value = (String) servletContext.getAttribute(key);
if(value == null){
value = reader.getManifestValue(attributeName);
if(StringUtils.isNotEmpty(value))
servletContext.setAttribute(key, value);
}
return value;
}
@Override
public String getVersionNumber() {
return getManifestValue("Implementation-Version");
}
public void setManifestReader(ManifestReader reader) {
this.reader = reader;
}
public ServletContext getServletContext() {
return servletContext;
}
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}
Create a common Action class (or add the same logic to a action with the same responsibility) that all actions that need this dynamically renamed css inherit from. ex. GenericAction.
public class GenericAction extends ActionSupport implements ServletRequestAware,
ServletResponseAware, ServletContextAware {
private ServletContext servletContext;
protected HttpServletRequest request;
protected HttpServletResponse response;
protected HttpSession session;
protected ManifestCache manifestCache;
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public void setServletRequest(HttpServletRequest httpServletRequest) {
this.request = httpServletRequest;
}
@Override
public void setServletResponse(HttpServletResponse httpServletResponse) {
this.response = httpServletResponse;
}
public HttpSession getSession(){
return request.getSession(true);
}
public ServletContext getServletContext() {
return servletContext;
}
public ManifestCache getManifestCache() {
return manifestCache;
}
public void setManifestCache(ManifestCache manifestCache) {
this.manifestCache = manifestCache;
}
public String getVersionNumber(){
return this.manifestCache.getVersionNumber();
}
public String getUrlFriendlyVersionNumber(){
return getVersionNumber().replace(".", "-");
}
}
In all jsp where you have your css link change to the following.
<link rel="stylesheet" type="text/css" href="/css/layout-<s:property value="urlFriendlyVersionNumber" default="static.css"/>">
Here we append the dynamically read property from GenericAction.getUrlFriendlyVersionNumber() to the css link path.
After this we now only need a Action which takes care of all requests made on “/css*”, disregards the version number and serves the actual “layout-static.css”.
public class VersionedCssAction extends GenericAction {
final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
@Qualifier("cssResource") private Resource resource;
private InputStream inputStream;
private final int CACHE_MAX_AGE = 86400;
private String resourcePath = "/css/layout-static.css";
@Action(value = "/css/*",
results = {
@Result(
name = ActionSupport.SUCCESS,
type = "stream",
params = {
"contentType", "text/css",
"inputName", "inputStream"
})
})
public String execute() {
response.setHeader("Cache-Control", String.format("max-age=%s", CACHE_MAX_AGE));
inputStream = getServletContext().getResourceAsStream(resourcePath);
return SUCCESS;
}
Thats it! As long as you create new versions for each deploy you will never get fluky behavior caused by css.