VIVO-1405 Add pseudo-version to requests for scripts and stylesheets.
The result is that the user does not need to refresh the browser cache when a new version of VIVO is deployed on the server.
This commit is contained in:
parent
0e15b9a69a
commit
b00633326e
2 changed files with 497 additions and 64 deletions
|
@ -2,82 +2,267 @@
|
||||||
|
|
||||||
package edu.cornell.mannlib.vitro.webapp.web.templatemodels;
|
package edu.cornell.mannlib.vitro.webapp.web.templatemodels;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
|
|
||||||
import freemarker.ext.beans.MethodAppearanceFineTuner;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
|
||||||
import freemarker.ext.beans.BeansWrapper;
|
import freemarker.ext.beans.BeansWrapper;
|
||||||
import freemarker.template.TemplateModel;
|
import freemarker.template.TemplateModel;
|
||||||
import freemarker.template.TemplateModelException;
|
import freemarker.template.TemplateModelException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a mechanism for Freemarker templates (main or included, parent or
|
||||||
|
* child) to add to the lists of scripts and style sheets for the current page.
|
||||||
|
*
|
||||||
|
* Each page uses 3 instances of Tags, exposed as ${scripts}, ${headScripts} and
|
||||||
|
* ${stylesheets}. A template may add a complete <script/$gt; element (for
|
||||||
|
* scripts or headScripts) or a <link> tag (for stylesheets), and these
|
||||||
|
* elements will appear at the proper location in the rendered HTML for the
|
||||||
|
* page.
|
||||||
|
*
|
||||||
|
* VIVO-1405: This process is augmented by the TagVersionInfo inner class, which
|
||||||
|
* attempts to add a "version=" query string to the URL in the supplied element.
|
||||||
|
* The version number is derived from the last-modified date of the specified
|
||||||
|
* script or stylesheet on the server. The effect is that a user's browser cache
|
||||||
|
* is effectively invalidated each time a new version of the script or
|
||||||
|
* stylesheet is deployed.
|
||||||
|
*/
|
||||||
public class Tags extends BaseTemplateModel {
|
public class Tags extends BaseTemplateModel {
|
||||||
|
private static final Log log = LogFactory.getLog(Tags.class);
|
||||||
|
|
||||||
private static final Log log = LogFactory.getLog(Tags.class);
|
protected final LinkedHashSet<String> tags;
|
||||||
|
|
||||||
protected final LinkedHashSet<String> tags;
|
public Tags() {
|
||||||
|
this.tags = new LinkedHashSet<String>();
|
||||||
|
}
|
||||||
|
|
||||||
public Tags() {
|
public Tags(LinkedHashSet<String> tags) {
|
||||||
this.tags = new LinkedHashSet<String>();
|
this.tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tags(LinkedHashSet<String> tags) {
|
public TemplateModel wrap() {
|
||||||
this.tags = tags;
|
try {
|
||||||
}
|
return new TagsWrapper().wrap(this);
|
||||||
|
} catch (TemplateModelException e) {
|
||||||
|
log.error("Error creating Tags template model");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public TemplateModel wrap() {
|
/**
|
||||||
try {
|
* Script and stylesheet lists are wrapped with a specialized BeansWrapper
|
||||||
return new TagsWrapper().wrap(this);
|
* that exposes certain write methods, instead of the configuration's object
|
||||||
} catch (TemplateModelException e) {
|
* wrapper, which doesn't. The templates can then add stylesheets and
|
||||||
log.error("Error creating Tags template model");
|
* scripts to the lists by calling their add() methods.
|
||||||
return null;
|
*
|
||||||
}
|
* @param Tags
|
||||||
}
|
* tags
|
||||||
|
* @return TemplateModel
|
||||||
|
*/
|
||||||
|
static public class TagsWrapper extends BeansWrapper {
|
||||||
|
|
||||||
/** Script and stylesheet lists are wrapped with a specialized BeansWrapper
|
public TagsWrapper() {
|
||||||
* that exposes certain write methods, instead of the configuration's object wrapper,
|
// Start by exposing all safe methods.
|
||||||
* which doesn't. The templates can then add stylesheets and scripts to the lists
|
setExposureLevel(EXPOSE_SAFE);
|
||||||
* by calling their add() methods.
|
}
|
||||||
*/
|
|
||||||
static public class TagsWrapper extends BeansWrapper {
|
|
||||||
|
|
||||||
public TagsWrapper() {
|
@SuppressWarnings("rawtypes")
|
||||||
// Start by exposing all safe methods.
|
@Override
|
||||||
setExposureLevel(EXPOSE_SAFE);
|
protected void finetuneMethodAppearance(Class cls, Method method,
|
||||||
setMethodAppearanceFineTuner(new MethodAppearanceFineTuner() {
|
MethodAppearanceDecision decision) {
|
||||||
@Override
|
|
||||||
public void process(MethodAppearanceDecisionInput methodAppearanceDecisionInput, MethodAppearanceDecision methodAppearanceDecision) {
|
|
||||||
try {
|
|
||||||
String methodName = methodAppearanceDecisionInput.getMethod().getName();
|
|
||||||
if ( ! ( methodName.equals("add") || methodName.equals("list")) ) {
|
|
||||||
methodAppearanceDecision.setExposeMethodAs(null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(e, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (!(methodName.equals("add") || methodName.equals("list"))) {
|
||||||
|
decision.setExposeMethodAs(null);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Template methods */
|
/* Template methods */
|
||||||
|
|
||||||
public void add(String... tags) {
|
@SuppressWarnings("hiding")
|
||||||
for (String tag : tags) {
|
public void add(String... tags) {
|
||||||
add(tag);
|
for (String tag : tags) {
|
||||||
}
|
add(tag);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void add(String tag) {
|
public void add(String tag) {
|
||||||
tags.add(tag);
|
TagVersionInfo info = new TagVersionInfo(tag);
|
||||||
}
|
if (info.hasVersion()) {
|
||||||
|
tags.add(TagVersionInfo.addVersionNumber(tag, info));
|
||||||
|
} else {
|
||||||
|
tags.add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String list() {
|
public String list() {
|
||||||
return StringUtils.join(tags, "\n");
|
return StringUtils.join(tags, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the value of "href" or "src".
|
||||||
|
*
|
||||||
|
* If there is such a value, and it doesn't have a query string already, and
|
||||||
|
* it represents a local URL, and we can locate the file that is served by
|
||||||
|
* the URL and get the last modified date, then we have found a "version
|
||||||
|
* number" that we can add to the attribute value.
|
||||||
|
*
|
||||||
|
* Reference for parsing attributes:
|
||||||
|
* https://www.w3.org/TR/html/syntax.html#elements-attributes
|
||||||
|
*/
|
||||||
|
protected static class TagVersionInfo {
|
||||||
|
private static final Pattern PATTERN_DOUBLE_QUOTES = Pattern
|
||||||
|
.compile("(href|src)\\s*=\\s*\"([^\"]+)\"[\\s|>]");
|
||||||
|
private static final int GROUP_INDEX_DOUBLE_QUOTES = 2;
|
||||||
|
|
||||||
|
private static final Pattern PATTERN_SINGLE_QUOTES = Pattern
|
||||||
|
.compile("(href|src)\\s*=\\s*'([^']+)'[\\s|>]");
|
||||||
|
private static final int GROUP_INDEX_SINGLE_QUOTES = 2;
|
||||||
|
|
||||||
|
private static final Pattern PATTERN_NO_QUOTES = Pattern
|
||||||
|
.compile("(href|src)\\s*=\\s*([^\"'<=>\\s]+)[\\s|>]");
|
||||||
|
private static final int GROUP_INDEX_NO_QUOTES = 2;
|
||||||
|
|
||||||
|
public static String addVersionNumber(String rawTag,
|
||||||
|
TagVersionInfo info) {
|
||||||
|
String versionString = (info.match.style == MatchResult.Style.NO_QUOTES)
|
||||||
|
? "?version&eq;"
|
||||||
|
: "?version=";
|
||||||
|
return rawTag.substring(0, info.match.start) + info.match.group
|
||||||
|
+ versionString + smushTimeStamp(info)
|
||||||
|
+ rawTag.substring(info.match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String smushTimeStamp(TagVersionInfo info) {
|
||||||
|
int smushed = (((char) (info.timestamp >> 48))
|
||||||
|
^ ((char) (info.timestamp >> 32))
|
||||||
|
^ ((char) (info.timestamp >> 16))
|
||||||
|
^ ((char) info.timestamp));
|
||||||
|
return String.format("%04x", smushed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MatchResult match;
|
||||||
|
private long timestamp = 0L;
|
||||||
|
|
||||||
|
public TagVersionInfo(String rawTag) {
|
||||||
|
try {
|
||||||
|
match = findUrlValue(rawTag);
|
||||||
|
|
||||||
|
if (match != null && !hasQueryString(match.group)) {
|
||||||
|
String stripped = stripContextPath(match.group);
|
||||||
|
|
||||||
|
if (stripped != null) {
|
||||||
|
String realPath = locateRealPath(stripped);
|
||||||
|
|
||||||
|
if (realPath != null) {
|
||||||
|
timestamp = getLastModified(realPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to add version info to tag: " + rawTag, e);
|
||||||
|
timestamp = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasVersion() {
|
||||||
|
return timestamp != 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MatchResult findUrlValue(String rawTag) {
|
||||||
|
Matcher mDouble = PATTERN_DOUBLE_QUOTES.matcher(rawTag);
|
||||||
|
if (mDouble.find()) {
|
||||||
|
return new MatchResult(mDouble, GROUP_INDEX_DOUBLE_QUOTES,
|
||||||
|
MatchResult.Style.DOUBLE_QUOTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher mSingle = PATTERN_SINGLE_QUOTES.matcher(rawTag);
|
||||||
|
if (mSingle.find()) {
|
||||||
|
return new MatchResult(mSingle, GROUP_INDEX_SINGLE_QUOTES,
|
||||||
|
MatchResult.Style.SINGLE_QUOTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher mNo = PATTERN_NO_QUOTES.matcher(rawTag);
|
||||||
|
if (mNo.find()) {
|
||||||
|
return new MatchResult(mNo, GROUP_INDEX_NO_QUOTES,
|
||||||
|
MatchResult.Style.NO_QUOTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(rawTag + " no match");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasQueryString(String group) {
|
||||||
|
if (group.indexOf('?') > -1) {
|
||||||
|
log.debug(group + " has query string already");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripContextPath(String group) {
|
||||||
|
String contextPath = UrlBuilder.getBaseUrl();
|
||||||
|
if (contextPath.isEmpty() || group.startsWith(contextPath)) {
|
||||||
|
return group.substring(contextPath.length());
|
||||||
|
} else {
|
||||||
|
log.debug(group + " doesn't match context path");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String locateRealPath(String stripped) {
|
||||||
|
ServletContext ctx = ApplicationUtils.instance()
|
||||||
|
.getServletContext();
|
||||||
|
String realPath = ctx.getRealPath(stripped);
|
||||||
|
if (realPath == null) {
|
||||||
|
log.debug(stripped + " has no real path");
|
||||||
|
}
|
||||||
|
return realPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getLastModified(String realPath) {
|
||||||
|
return new File(realPath).lastModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class MatchResult {
|
||||||
|
public enum Style {
|
||||||
|
SINGLE_QUOTES, DOUBLE_QUOTES, NO_QUOTES
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String group;
|
||||||
|
public final int start;
|
||||||
|
public final int end;
|
||||||
|
public final Style style;
|
||||||
|
|
||||||
|
public MatchResult(Matcher matcher, int group, Style style) {
|
||||||
|
this.group = matcher.group(group);
|
||||||
|
this.start = matcher.start(group);
|
||||||
|
this.end = matcher.end(group);
|
||||||
|
this.style = style;
|
||||||
|
log.debug(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "MatchResult[start=" + start + ", end=" + end
|
||||||
|
+ ", group=" + group + ", style=" + style + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
package edu.cornell.mannlib.vitro.webapp.web.templatemodels;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
import org.apache.log4j.Level;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.Tags.TagVersionInfo;
|
||||||
|
import edu.cornell.mannlib.vitro.webapp.web.templatemodels.Tags.TagVersionInfo.MatchResult;
|
||||||
|
import stubs.edu.cornell.mannlib.vitro.webapp.modules.ApplicationStub;
|
||||||
|
import stubs.javax.servlet.ServletContextStub;
|
||||||
|
|
||||||
|
public class TagsTest extends AbstractTestClass {
|
||||||
|
private ServletContextStub ctx;
|
||||||
|
private File resource;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws IOException {
|
||||||
|
resource = File.createTempFile("resource", "");
|
||||||
|
|
||||||
|
ctx = new ServletContextStub();
|
||||||
|
ctx.setRealPath("/base/sub/file.js", resource.getPath());
|
||||||
|
ctx.setRealPath("/base/sub/file.css", resource.getPath());
|
||||||
|
|
||||||
|
ApplicationStub.setup(ctx, null);
|
||||||
|
|
||||||
|
setContextPath("/context");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Parsing tests
|
||||||
|
//
|
||||||
|
// Reference for parsing attributes:
|
||||||
|
// https://www.w3.org/TR/html/syntax.html#elements-attributes
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noAttribute_failure() {
|
||||||
|
assertNoMatch("<div height='value'></div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singleQuote_noTerminator_failure() {
|
||||||
|
assertNoMatch("<link rel='stylesheet' href='problem></link>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singleQuotes_embeddedSingleQuote_failure() {
|
||||||
|
assertNoMatch("<script src='value'problem'></script");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singleQuotes_embeddedDoubleQuote_success() {
|
||||||
|
assertMatch("<script src='value\"noproblem'></script",
|
||||||
|
"value\"noproblem");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleQuote_noTerminator_failure() {
|
||||||
|
assertNoMatch("<link rel=\"stylesheet\" href=\"problem ></link>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleQuotes_embeddedDoubleQuote_failure() {
|
||||||
|
assertNoMatch("<link href=\"value\"problem\"></link>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleQuotes_embeddedSingleQuote_success() {
|
||||||
|
assertMatch("<link href=\"value'noproblem\"></link>",
|
||||||
|
"value'noproblem");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquotedBadTerminator_failure() {
|
||||||
|
assertNoMatch("<script src=problem");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquoted_embeddedEquals_failure() {
|
||||||
|
assertNoMatch("<script src=value=problem>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquoted_embeddedSingleQuote_failure() {
|
||||||
|
assertNoMatch("<script src=value'problem>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquoted_embeddedDoubleQuote_failure() {
|
||||||
|
assertNoMatch("<script src=value\"problem>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquoted_embeddedBackTick_failure() {
|
||||||
|
assertNoMatch("<script src=value`problem>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquoted_embeddedLessThan_failure() {
|
||||||
|
assertNoMatch("<script src=value<problem>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void spacesBeforeEquals_success() {
|
||||||
|
assertMatch("<link href =value rel='stylesheet'>", "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void spacesAfterEquals_success() {
|
||||||
|
assertMatch("<script src= 'value'></script>", "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noSpacesAroundEquals_success() {
|
||||||
|
assertMatch("<script src=\"value\" ></script>", "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Substitution tests
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noMatch_noChange() {
|
||||||
|
assertVersionNotAdded(
|
||||||
|
"<script junk='/context/base/sub/file.js' ></script>",
|
||||||
|
"no match");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void alreadyHasQueryString_noChange() {
|
||||||
|
assertVersionNotAdded(
|
||||||
|
"<script src='/context/base/sub/file.js?why' ></script>",
|
||||||
|
"has query");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doesntStartWithContextPath_noChange() {
|
||||||
|
assertVersionNotAdded(
|
||||||
|
"<script src='/notContext/base/sub/file.js' ></script>",
|
||||||
|
"context path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noRealPath_noChange() {
|
||||||
|
assertVersionNotAdded(
|
||||||
|
"<script src='/context/base/sub/nofile.js' ></script>",
|
||||||
|
"real path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore
|
||||||
|
public void exception_noChange() {
|
||||||
|
fail("exception_noChange not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleQuotes_substitution() {
|
||||||
|
assertVersionAdded( //
|
||||||
|
"<link href=\"/context/base/sub/file.css\" rel=stylesheet></link>", //
|
||||||
|
"<link href=\"/context/base/sub/file.css?version=9999\" rel=stylesheet></link>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singleQuotes_substitution() {
|
||||||
|
assertVersionAdded( //
|
||||||
|
"<script src='/context/base/sub/file.js' ></script>", //
|
||||||
|
"<script src='/context/base/sub/file.js?version=9999' ></script>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unquoted_substitution() {
|
||||||
|
assertVersionAdded( //
|
||||||
|
"<script type=text/javascript src=/context/base/sub/file.js ></script>", //
|
||||||
|
"<script type=text/javascript src=/context/base/sub/file.js?version&eq;9999 ></script>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Helper methods
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void setContextPath(String contextPath) {
|
||||||
|
try {
|
||||||
|
Field f = UrlBuilder.class.getDeclaredField("contextPath");
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(null, contextPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMatch(String tag, String expected) {
|
||||||
|
TagVersionInfo info = new TagVersionInfo(tag);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Field f = TagVersionInfo.class.getDeclaredField("match");
|
||||||
|
f.setAccessible(true);
|
||||||
|
MatchResult match = (MatchResult) f.get(info);
|
||||||
|
|
||||||
|
assertEquals(expected, match.group);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNoMatch(String tag) {
|
||||||
|
TagVersionInfo info = new TagVersionInfo(tag);
|
||||||
|
assertFalse(info.hasVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertVersionAdded(String rawTag, String expected) {
|
||||||
|
String actual = createTag(rawTag);
|
||||||
|
String canonicalActual = actual.replaceAll("=[0-9a-f]{4}", "=9999")
|
||||||
|
.replaceAll("&eq;[0-9a-f]{4}", "&eq;9999");
|
||||||
|
assertEquals(expected, canonicalActual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertVersionNotAdded(String rawTag, String debugMessage) {
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
captureLogOutput(Tags.class, writer, true);
|
||||||
|
setLoggerLevel(Tags.class, Level.DEBUG);
|
||||||
|
|
||||||
|
String actual = createTag(rawTag);
|
||||||
|
assertEquals(rawTag, actual);
|
||||||
|
assertThat(writer.toString(), containsString(debugMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createTag(String rawTag) {
|
||||||
|
Tags t = new Tags();
|
||||||
|
t.add(rawTag);
|
||||||
|
return t.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue