Skip to content

Instantly share code, notes, and snippets.

@Crydust
Created September 5, 2013 11:21
Show Gist options
  • Save Crydust/6448900 to your computer and use it in GitHub Desktop.
Save Crydust/6448900 to your computer and use it in GitHub Desktop.
ValidatorFilter uses local validator.nu to validate html
package be.crydust.validator;
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public final class ValidatorFilter implements Filter {
String serviceUrl = "http://validator.nu/";
List<String> ignoreRequestIfUriContains = Collections.emptyList();
List<String> ignoreRequestIfUriEndsWith = Collections.emptyList();
List<String> ignoredMessages = Collections.emptyList();
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("*****************************************************");
System.out.println("ValidatorFilter");
System.out.println("");
if (!isNullOrEmpty(filterConfig.getInitParameter("serviceUrl"))) {
serviceUrl = filterConfig.getInitParameter("serviceUrl");
}
if (!isNullOrEmpty(filterConfig
.getInitParameter("ignoreRequestIfUriContains"))) {
ignoreRequestIfUriContains = Arrays.asList(filterConfig
.getInitParameter("ignoreRequestIfUriContains").split(";"));
}
if (!isNullOrEmpty(filterConfig
.getInitParameter("ignoreRequestIfUriEndsWith"))) {
ignoreRequestIfUriEndsWith = Arrays.asList(filterConfig
.getInitParameter("ignoreRequestIfUriEndsWith").split(";"));
}
if (!isNullOrEmpty(filterConfig.getInitParameter("ignoredMessages"))) {
ignoredMessages = Arrays.asList(filterConfig.getInitParameter(
"ignoredMessages").split(";"));
}
System.out.printf("serviceUrl = %s%n", serviceUrl);
System.out.printf("ignoreRequestIfUriContains = %n* %s%n",
join("\n* ", ignoreRequestIfUriContains));
System.out.printf("ignoreRequestIfUriEndsWith = %n* %s%n",
join("\n* ", ignoreRequestIfUriEndsWith));
System.out.printf("ignoredMessages = %n* %s%n",
join("\n* ", ignoredMessages));
System.out.println("*****************************************************");
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String requestUri = req.getRequestURI();
boolean ignoreRequest = false;
for (String s : ignoreRequestIfUriContains) {
if (!s.isEmpty() && requestUri.contains(s)) {
ignoreRequest = true;
}
}
for (String s : ignoreRequestIfUriEndsWith) {
if (!s.isEmpty() && requestUri.endsWith(s)) {
ignoreRequest = true;
}
}
if (ignoreRequest) {
chain.doFilter(req, res);
return;
}
ResponseBuffer buffer = new ResponseBuffer(res);
chain.doFilter(request, buffer);
String contents = buffer.toString();
// validate
// see http://wiki.whatwg.org/wiki/Validator.nu_Web_Service_Interface
StringBuilder html = new StringBuilder();
try {
// send page sourcecode to service
String url = serviceUrl + "?out=xml&showsource=yes";
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Accept-Charset", "UTF-8");
connection.setRequestProperty("Content-Type", "text/html");
try (OutputStream output = connection.getOutputStream();) {
output.write(contents.getBytes("UTF-8"));
}
// read the reply from the web service
final XPathFactory factory = XPathFactory.newInstance();
NamespaceContext ns = new ValidatorNamespaceContext();
Document document = readXmlDocument(connection.getInputStream());
// find relevant xml nodes
NodeList infoNodes = getNodesByXPath(factory, ns, document,
"/:messages/:info");
NodeList errorNodes = getNodesByXPath(factory, ns, document,
"/:messages/:error");
NodeList nonDocumentErrorNodes = getNodesByXPath(factory, ns,
document, "/:messages/:non-document-error");
NodeList infoOrErrorNodes = getNodesByXPath(factory, ns, document,
"/:messages/:info|/:messages/:error");
String source = getNodesByXPath(factory, ns, document,
"/:messages/:source/text()").item(0).getNodeValue();
// the little box top left of the page with onclick
// remember where to insert the "bad errors count"
html.append("<div")
.append(" onclick='(function(){var s = document.getElementById(\"validationFilterMessages\").style; s.visibility = (s.visibility === \"hidden\" ? \"visible\" : \"hidden\");}());'")
.append("style='z-index:1000;position:absolute;top:0px;left:0;font-size:12px;color:#000;background:#ccc;width:200px;height:40px;overflow:hidden;'>")
.append("<b>bad errors: ");
int badErrorsInsertionPoint = html.length();
html.append(
String.format(
"</b><br /><small>error: %d, info: %d, other: %d</small>",
errorNodes.getLength(), infoNodes.getLength(),
nonDocumentErrorNodes.getLength()))
.append("</div>");
// the big box with messages (hidden for now)
// begin #validationFilterMessages
html.append(
"<div id='validationFilterMessages' style='visibility:hidden;:z-index:1000;position:absolute;top:40px;left:0;font-size:16px;color:#000;background:#fff;width:1024px;height:600px;overflow:scroll;'>")
.append("<ol>");
// loop over messages and counting the non-ignored ones
int badErrors = 0;
for (int i = 0, leni = infoOrErrorNodes.getLength(); i < leni; i++) {
Node infoOrErrorNode = infoOrErrorNodes.item(i);
boolean isError = "error".equals(infoOrErrorNode.getNodeName());
Node messageNode = getNodesByXPath(factory, ns,
infoOrErrorNode, "./:message").item(0);
Node extractNode = null;
if (isError) {
extractNode = getNodesByXPath(factory, ns, infoOrErrorNode,
"./:extract").item(0);
}
String messageText = messageNode.getTextContent();
// line and column number (-1 for the ones not given)
int firstLine = Integer.parseInt(getAttributeOrDefault(
infoOrErrorNode, "first-line", "-1"));
int firstColumn = Integer.parseInt(getAttributeOrDefault(
infoOrErrorNode, "first-column", "-1"));
int lastLine = Integer.parseInt(getAttributeOrDefault(
infoOrErrorNode, "last-line", "-1"));
int lastColumn = Integer.parseInt(getAttributeOrDefault(
infoOrErrorNode, "last-column", "-1"));
boolean isIgnored = ignoredMessages.contains(messageText);
if (isError && !isIgnored) {
badErrors++;
}
String backgroundColor = isIgnored ? "#CCC" : (isError ? "#FCC"
: "#CFF");
String color = isIgnored ? "#999" : "#000";
html.append("<li style='").append("color:").append(color)
.append(";background-color:");
// message
html.append(backgroundColor).append(";'><b>")
.append(isError ? "Error" : "Info").append(": </b>")
.append(escapeHtml4(messageText));
// link to line in sourcecode
if (lastLine != -1 && !isIgnored) {
html.append("<br />");
if (firstLine != -1) {
html.append(String
.format("<a href='#sourceLine%d'><small>From line %d, column %d; to line %d, column %d</small></a>",
firstLine, firstLine, firstColumn,
lastLine, lastColumn));
} else {
html.append(String
.format("<a href='#sourceLine%d'><small>At line %d, column %d</small></a>",
lastLine, lastLine, lastColumn));
}
}
// extract
if (isError && !isIgnored && extractNode != null) {
html.append("<br /><pre style='margin:0;'>");
NodeList extractChildNodes = extractNode.getChildNodes();
for (int j = 0, lenj = extractChildNodes.getLength(); j < lenj; j++) {
Node extractChildNode = extractChildNodes.item(j);
if ("#text".equals(extractChildNode.getNodeName())) {
html.append(escapeHtml4(extractChildNode
.getNodeValue().replaceAll("\r?\n",
"\u21a9")));
} else {
html.append("<b style='background:#f66;'>")
.append(escapeHtml4(extractChildNode
.getFirstChild().getNodeValue()
.replaceAll("\r?\n", "\u21a9")))
.append("</b>");
}
}
html.append("</pre>");
}
html.append("</li>");
}
html.append("</ol>");
// the non-ignored erros are now counted
html.insert(badErrorsInsertionPoint, badErrors);
// write sourcecode line by line with anchors
String[] sourceLines = source.split("\r?\n");
html.append("<ol>");
int lineNumber = 1;
for (String sourceLine : sourceLines) {
html.append("<li id='sourceLine").append(lineNumber)
.append("'><pre style='margin:0;'>")
.append(escapeHtml4(sourceLine)).append("</pre>");
lineNumber++;
}
html.append("</ol>");
// end #validationFilterMessages
html.append("</div>");
} catch (Exception e) {
e.printStackTrace();
}
try {
PrintWriter printWriter = res.getWriter();
printWriter.write(contents);
printWriter.write(html.toString());
printWriter.close();
} catch (IllegalStateException ex) {
OutputStream outputStream = res.getOutputStream();
outputStream.write(contents.getBytes("UTF-8"));
outputStream.write(html.toString().getBytes("UTF-8"));
outputStream.close();
}
}
public void destroy() {
// NOOP
}
private static class ValidatorNamespaceContext implements NamespaceContext {
public String getPrefix(String namespaceURI) {
return null;
}
@SuppressWarnings("rawtypes")
public Iterator getPrefixes(String namespaceURI) {
return null;
}
public String getNamespaceURI(String prefix) {
if (prefix == null)
throw new NullPointerException("Invalid Namespace Prefix");
else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix))
return "http://n.validator.nu/messages/";
else if ("h".equals(prefix))
return "http://www.w3.org/1999/xhtml";
else
return XMLConstants.NULL_NS_URI;
}
}
private static NodeList getNodesByXPath(XPathFactory factory,
NamespaceContext ns, Object item, String expression)
throws XPathExpressionException {
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(ns);
XPathExpression expr = xpath.compile(expression);
Object result = expr.evaluate(item, XPathConstants.NODESET);
return (NodeList) result;
}
private static String getAttributeOrDefault(Node node, String name,
String defaultValue) {
NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
Node attribute = attributes.getNamedItem(name);
if (attribute != null) {
return attribute.getNodeValue();
}
}
return defaultValue;
}
private static Document readXmlDocument(InputStream is)
throws ParserConfigurationException, SAXException, IOException {
if (is == null) {
return null;
}
Document document = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(is);
return document;
}
private static class ResponseBuffer extends HttpServletResponseWrapper {
private CharArrayWriter buffer;
public String toString() {
return buffer.toString();
}
public PrintWriter getWriter() {
return new PrintWriter(buffer);
}
public ResponseBuffer(HttpServletResponse response) {
super(response);
buffer = new CharArrayWriter();
}
}
private static boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}
private static String join(String delimiter, List<String> s) {
StringBuilder builder = new StringBuilder();
Iterator<?> iter = s.iterator();
while (iter.hasNext()) {
builder.append(iter.next());
if (!iter.hasNext()) {
break;
}
builder.append(delimiter);
}
return builder.toString();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<!-- ... -->
<filter>
<filter-name>ValidatorFilter</filter-name>
<filter-class>be.crydust.validator.ValidatorFilter</filter-class>
<init-param>
<param-name>serviceUrl</param-name>
<param-value>http://192.168.56.101:8888/</param-value>
</init-param>
<init-param>
<param-name>ignoredMessages</param-name>
<param-value>The cellspacing attribute on the table element is obsolete. Use CSS instead.;The cellpadding attribute on the table element is obsolete. Use CSS instead.</param-value>
</init-param>
</filter>
<!-- ... -->
<filter-mapping>
<filter-name>ValidatorFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- ... -->
</web-app>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment