Programming Thoughts
Struts 2 - Miscellaneous
Code File Caching
CSS and JavaScript files can be cached
Struts-based web apps rely on static CSS and JavaScript files for formatting and DOM manipulation. They can be cached but there are problems.
Code File Caching
Struts-based web apps generate HTML in the server but typically rely on separately downloaded, static CSS and JavaScript files for formatting and DOM manipulation. As they're static, they can be cached but as the web app is updated with a new version, stale files can result in formatting glitches and malfunctioning interaction. Inserting the version number into filenames is standard practice for libraries but web app version numbers change far too frequently to rename every file and update links in every JSP. Worse, such files for unstable, snapshot versions can change with every build.
There are Maven plugins to insert version numbers into files but web browsers don't read them. If the Struts Action can present the app version, it can be used as a fake parameter, like the following.
<SCRIPT SRC="/front-admin/js/utilities.js?version=<s:property value="downloadVersion" />" TYPE="text/javascript"></SCRIPT>
Actions, particularly Viewer Actions, must implement the interface below. The downloadVersion
property amalgamates the app title and version. However, if the version is a snapshot version, the code in
the file is regarded as unstable and the system time ensures it's loaded after each build. This can include
files from other web apps.
public interface AppVersionAware { public String getAppTitle(); public void setAppTitle(String value); public String getAppVendor(); public void setAppVendor(String value); public String getAppVersion(); public void setAppVersion(String value); public default String getDownloadVersion() { String result; if (getAppTitle() != null && getAppTitle().length() > 0 && getAppVersion() != null && getAppVersion().length() > 0) { try { result = URLEncoder.encode(getAppTitle(), StandardCharsets.UTF_8.name()) + "-" + getAppVersion(); if (result.endsWith("-SNAPSHOT")) { result = result + "-" + System.currentTimeMillis(); } } catch (UnsupportedEncodingException e) { result = Long.toString(System.currentTimeMillis()); } } else { result = Long.toString(System.currentTimeMillis()); } return result; } }
Note the above doesn't work with Struts 2.4 or earlier as its version of OGNL doesn't scan interfaces for
default methods, so won't find the downloadVersion property. If using such an old version, the
workaround is to have all implementing classes implement
public getDownloadVersion() { return AppVersionAware.super.getDownloadVersion() }
Interceptor
Struts 2 is designed around interceptors, so setting version numbers is done with one.
Class.getPackage() may seem the obvious way to get it but it's vulnerable to weird and wonderful
classloaders. Instead, as web apps come in WAR files, the MANIFEST.MF file is read once.
private synchronized void loadManifestIfNeeded(ActionInvocation invocation) { if (manifestLoaded) { return; } try (InputStream is = ServletActionContext.getServletContext().getResourceAsStream("/META-INF/MANIFEST.MF")) { if (is != null) { Attributes attrs = new Manifest(is).getMainAttributes(); manifestTitle = attrs.getValue(Attributes.Name.IMPLEMENTATION_TITLE); manifestVendor = attrs.getValue(Attributes.Name.IMPLEMENTATION_VENDOR); manifestVersion = attrs.getValue(Attributes.Name.IMPLEMENTATION_VERSION); manifestLoaded = true; } } catch (Exception e) { long now = System.currentTimeMillis(); if (now - lastErrorLogTime >= ERROR_LOG_INTERVAL) { lastErrorLogTime = now; LOG.error("Read of app MANIFEST.MF failed " + " Struts action=" + invocation.getAction().getClass(), e); } } }
The intercept function pretty much writes itself. Note the use of volatile as well as synchronized above as interceptors aren't thread safe.
private boolean disabled; private volatile boolean manifestLoaded = false; private volatile String manifestTitle; private volatile String manifestVendor; private volatile String manifestVersion; private static final long ERROR_LOG_INTERVAL = 600000L; // 10 minutes private long lastErrorLogTime = 0; @Override public String intercept(ActionInvocation invocation) throws Exception { if (!disabled && invocation.getAction() instanceof AppVersionAware) { if (!manifestLoaded) { loadManifestIfNeeded(invocation); } AppVersionAware appVersionAware = (AppVersionAware) invocation.getAction(); appVersionAware.setAppTitle(manifestTitle); appVersionAware.setAppVendor(manifestVendor); appVersionAware.setAppVersion(manifestVersion); } return invocation.invoke(); }