Skip to content

Instantly share code, notes, and snippets.

@aldrinleal
Created July 3, 2014 09:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aldrinleal/925b2cdfbf91308fe484 to your computer and use it in GitHub Desktop.
Save aldrinleal/925b2cdfbf91308fe484 to your computer and use it in GitHub Desktop.
JSON (com Jackson) para SAX, Mockito-fu, Java 8, e InvocationHandlers para testes e a sua própria manutenção!
// Fonte Original: https://github.com/lukas-krecan/json2xml/blob/master/src/main/java/net/javacrumbs/json2xml/JsonSaxAdapter.java
import com.google.common.collect.ImmutableMap;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import java.util.Map;
import static com.fasterxml.jackson.core.JsonToken.END_ARRAY;
import static com.fasterxml.jackson.core.JsonToken.END_OBJECT;
import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME;
import static com.fasterxml.jackson.core.JsonToken.START_ARRAY;
import static com.fasterxml.jackson.core.JsonToken.START_OBJECT;
import static com.fasterxml.jackson.core.JsonToken.VALUE_FALSE;
import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL;
import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_FLOAT;
import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT;
import static com.fasterxml.jackson.core.JsonToken.VALUE_STRING;
import static com.fasterxml.jackson.core.JsonToken.VALUE_TRUE;
/**
* Converts JSON to SAX events. It can be used either directly
* <pre>
* <code>
* ContentHandler ch = ...;
* JsonSaxAdapter adapter = new JsonSaxAdapter(JsonSaxAdapterTest.JSON, ch);
* adapter.parse();
* </code>
* </pre>
*/
public class JsonSaxAdapter {
public static final AttributesImpl INT_ATTRIBUTES = asAttributes("int");
public static final AttributesImpl FLOAT_ATTRIBUTES = asAttributes("float");
public static final AttributesImpl BOOLEAN_ATTRIBUTES = asAttributes("boolean");
public static final AttributesImpl STRING_ATTRIBUTES = asAttributes("string");
public static final AttributesImpl NULL_ATTRIBUTES = asAttributes("null");
public static final AttributesImpl ARRAY_ATTRIBUTES = asAttributes("array");
public static final AttributesImpl EMPTY_ATTRIBUTES = asAttributes(null);
private static final JsonFactory JSON_FACTORY = new JsonFactory();
private static final Map<JsonToken, Attributes> VALID_ATTRIBUTES = new ImmutableMap.Builder<JsonToken, Attributes>().//
put(VALUE_NUMBER_INT, INT_ATTRIBUTES).//
put(VALUE_NUMBER_FLOAT, FLOAT_ATTRIBUTES).//
put(VALUE_FALSE, BOOLEAN_ATTRIBUTES).//
put(VALUE_TRUE, BOOLEAN_ATTRIBUTES).//
put(VALUE_STRING, STRING_ATTRIBUTES).//
put(VALUE_NULL, NULL_ATTRIBUTES).//
put(START_ARRAY, ARRAY_ATTRIBUTES).//
build();
private final JsonParser jsonParser;
private final ContentHandler contentHandler;
private final boolean addTypeAttributes;
private final String artificialRootName;
/**
* Creates JsonSaxAdapter that coverts JSON to SAX events.
*
* @param json JSON to parse
* @param contentHandler target of SAX events
*/
public JsonSaxAdapter(final String json, final ContentHandler contentHandler) {
this(parseJson(json), contentHandler);
}
/**
* Creates JsonSaxAdapter that coverts JSON to SAX events.
*
* @param jsonParser parsed JSON
* @param contentHandler target of SAX events
*/
public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler) {
this(jsonParser, contentHandler, true, "ROOT");
}
/**
* Creates JsonSaxAdapter that coverts JSON to SAX events.
*
* @param jsonParser parsed JSON
* @param contentHandler target of SAX events
* @param addTypeAttributes adds type information as attributes
* @param artificialRootName if set, an artificial root is generated so JSON documents with more
* roots can be handeled.
*/
public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler,
final boolean addTypeAttributes, final String artificialRootName) {
this.jsonParser = jsonParser;
this.contentHandler = contentHandler;
this.addTypeAttributes = addTypeAttributes;
this.artificialRootName = artificialRootName;
contentHandler.setDocumentLocator(new DocumentLocator());
}
private static JsonParser parseJson(final String json) {
try {
return JSON_FACTORY.createParser(json);
} catch (Exception e) {
throw new JsonParserException("Parsing error", e);
}
}
static AttributesImpl asAttributes(String k) {
AttributesImpl attributes = new AttributesImpl();
if (null != k)
attributes.addAttribute("", "type", "type", "string", k);
return attributes;
}
/**
* Method parses JSON and emits SAX events.
*/
public void parse() throws JsonParserException {
try {
jsonParser.nextToken();
contentHandler.startDocument();
if (shouldAddArtificialRoot()) {
startElement(artificialRootName);
parseElement(artificialRootName, false);
endElement(artificialRootName);
} else if (START_OBJECT.equals(jsonParser.getCurrentToken())) {
int elementsWritten = parseObject();
if (elementsWritten > 1) {
throw new JsonParserException(
"More than one root element. Can not generate legal XML. You can set artificialRootName to generate an artificial root.");
}
} else {
throw new JsonParserException(
"Unsupported root element. Can not generate legal XML. You can set artificialRootName to generate an artificial root.");
}
contentHandler.endDocument();
} catch (Exception e) {
throw new JsonParserException("Parsing error: " + e.getMessage(), e);
}
}
private boolean shouldAddArtificialRoot() {
return artificialRootName != null && artificialRootName.length() > 0;
}
/**
* Parses generic object.
*
* @return number of elements written
*/
private int parseObject() throws Exception {
int elementsWritten = 0;
while (jsonParser.nextToken() != null && jsonParser.getCurrentToken() != END_OBJECT) {
if (FIELD_NAME.equals(jsonParser.getCurrentToken())) {
String elementName = jsonParser.getCurrentName();
//jump to element value
jsonParser.nextToken();
startElement(elementName);
parseElement(elementName, false);
endElement(elementName);
elementsWritten++;
} else {
throw new JsonParserException(
"Error when parsing. Expected field name got " + jsonParser.getCurrentToken());
}
}
return elementsWritten;
}
/**
* Pares JSON element.
*
* @param inArray if the element is in an array
*/
private void parseElement(final String elementName, final boolean inArray) throws Exception {
JsonToken currentToken = jsonParser.getCurrentToken();
if (inArray) {
startElement(elementName);
}
if (START_OBJECT.equals(currentToken)) {
parseObject();
} else if (START_ARRAY.equals(currentToken)) {
parseArray(elementName);
} else if (currentToken.isScalarValue()) {
parseValue();
}
if (inArray) {
endElement(elementName);
}
}
private void parseArray(final String elementName) throws Exception {
while (jsonParser.nextToken() != END_ARRAY && jsonParser.getCurrentToken() != null) {
parseElement(elementName, true);
}
}
private void parseValue() throws Exception {
if (VALUE_NULL != jsonParser.getCurrentToken()) {
String text = jsonParser.getText();
contentHandler.characters(text.toCharArray(), 0, text.length());
}
}
private void startElement(final String elementName) throws SAXException {
contentHandler.startElement(null, elementName, elementName, getTypeAttributes());
}
private Attributes getTypeAttributes() {
Attributes attributes = addTypeAttributes ? VALID_ATTRIBUTES.get(jsonParser.getCurrentToken()) : null;
return null == attributes ? EMPTY_ATTRIBUTES : attributes;
}
private void endElement(final String elementName) throws SAXException {
contentHandler.endElement(null, elementName, elementName);
}
public static class JsonParserException extends RuntimeException {
private static final long serialVersionUID = 2194022343599245018L;
public JsonParserException(final String message, final Throwable cause) {
super(message, cause);
}
public JsonParserException(final String message) {
super(message);
}
public JsonParserException(final Throwable cause) {
super(cause);
}
}
public class DocumentLocator implements Locator {
public String getPublicId() {
Object sourceRef = jsonParser.getCurrentLocation().getSourceRef();
if (sourceRef != null) {
return sourceRef.toString();
} else {
return "";
}
}
public String getSystemId() {
return getPublicId();
}
public int getLineNumber() {
return jsonParser.getCurrentLocation() != null ? jsonParser.getCurrentLocation().getLineNr()
: -1;
}
public int getColumnNumber() {
return jsonParser.getCurrentLocation() != null ? jsonParser.getCurrentLocation().getColumnNr()
: -1;
}
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.io.ClassPathResource;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import java.lang.reflect.Proxy;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class JsonSaxAdapterTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private ClassPathResource resource;
private JsonSaxAdapter adapter;
@Mock
private ContentHandler handler;
@Before
public void before() throws Exception {
this.resource = new ClassPathResource("payload.json");
}
@Test
public void sanityTest() throws Exception {
RecorderInvocationHandler recorder = new RecorderInvocationHandler();
ContentHandler contentHandlerCallGenerator = ContentHandler.class.cast(
Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{ContentHandler.class}, recorder));
this.adapter =
new JsonSaxAdapter(OBJECT_MAPPER.getFactory().createParser(resource.getInputStream()),
contentHandlerCallGenerator);
this.adapter.parse();
recorder.getInvocations().forEach(System.out::println);
}
@Test
public void testReader() throws Exception {
this.adapter =
new JsonSaxAdapter(OBJECT_MAPPER.getFactory().createParser(resource.getInputStream()),
handler);
this.adapter.parse();
verify(handler).setDocumentLocator(any(Locator.class));
verify(handler).startDocument();
verify(handler).startElement(null, "ROOT", "ROOT", JsonSaxAdapter.EMPTY_ATTRIBUTES);
verify(handler, times(3)).startElement(null, "id", "id", JsonSaxAdapter.STRING_ATTRIBUTES);
verify(handler, times(3)).endElement(null, "id", "id");
verify(handler).startElement(null, "slots", "slots", JsonSaxAdapter.ARRAY_ATTRIBUTES);
verify(handler).startElement(null, "slots", "slots", JsonSaxAdapter.EMPTY_ATTRIBUTES);
verify(handler).startElement(null, "_type", "_type", JsonSaxAdapter.STRING_ATTRIBUTES);
verify(handler).characters("BannerSlot".toCharArray(), 0, 10);
verify(handler).endElement(null, "_type", "_type");
verify(handler, times(2)).characters("1".toCharArray(), 0, 1);
verify(handler, times(3)).endElement(null, "id", "id");
verify(handler).endDocument();
}
@Test
public void testUsingGeneratedStuff() throws Exception {
this.adapter =
new JsonSaxAdapter(OBJECT_MAPPER.getFactory().createParser(resource.getInputStream()),
handler);
this.adapter.parse();
verify(handler, times(1)).setDocumentLocator(any(Locator.class));
verify(handler, times(1)).startDocument();
verify(handler, times(1)).startElement(null, "ROOT", "ROOT", JsonSaxAdapter.EMPTY_ATTRIBUTES);
verify(handler, times(3)).startElement(null, "id", "id", JsonSaxAdapter.STRING_ATTRIBUTES);
verify(handler, times(3)).endElement(null, "id", "id");
verify(handler, times(1)).startElement(null, "slots", "slots", JsonSaxAdapter.ARRAY_ATTRIBUTES);
verify(handler, times(1)).startElement(null, "slots", "slots", JsonSaxAdapter.EMPTY_ATTRIBUTES);
verify(handler, times(1)).startElement(null, "_type", "_type", JsonSaxAdapter.STRING_ATTRIBUTES);
/// Linhas. Muitas linhas.
verify(handler, times(1)).endElement(null, "location", "location");
verify(handler, times(1)).endElement(null, "ROOT", "ROOT");
verify(handler, times(1)).endDocument();
}
}
import com.google.common.base.Joiner;
import org.apache.commons.lang.StringEscapeUtils;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.lang.String.format;
public class RecorderInvocationHandler implements InvocationHandler {
Map<String, Integer> occurrenceMap = new LinkedHashMap<String, Integer>();
public List<String> getInvocations() {
Function<Map.Entry<String, Integer>, String>
callFormatter =
pair -> format("verify(handler, times(%d)).%s", pair.getValue(), pair.getKey());
return occurrenceMap.entrySet().stream().map(callFormatter).collect(Collectors.toList());
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String argsAsStr;
if (null == args || 0 == args.length) {
argsAsStr = "";
} else {
List<String> argsList = new ArrayList<String>(args.length);
for (Object o : args)
argsList.add(formatObj(o));
argsAsStr = Joiner.on(", ").join(argsList);
}
String invocation = format("%s(%s);", method.getName(), argsAsStr);
int count = (occurrenceMap.containsKey(invocation) ? occurrenceMap.get(invocation) : 0);
occurrenceMap.put(invocation, 1 + count);
System.err.println(invocation);
return null;
}
public String formatObj(Object obj) {
if (null == obj) {
return "null";
}
if (obj instanceof String) {
return format("\"%s\"", StringEscapeUtils.escapeJava("" + obj));
} else if (obj instanceof char[]) {
return format("\"%s\".toCharArray()", StringEscapeUtils.escapeJava(new String((char[]) obj)));
} else if (obj instanceof Number) {
return "" + obj;
} else if (obj instanceof Locator) {
return "any(Locator.class)";
} else if (obj instanceof Attributes) {
Attributes attr = (Attributes) obj;
if (1 == attr.getLength())
return format("JsonSaxAdapter.%s_ATTRIBUTES", attr.getValue("type").toUpperCase());
if (attr == JsonSaxAdapter.EMPTY_ATTRIBUTES)
return "JsonSaxAdapter.EMPTY_ATTRIBUTES";
return "any(Attributes.class";
} else {
System.err.println("Unhandled type: " + obj.getClass().getName());
return obj.toString();
}
}
}
// Saída do Unit Test
// Parte 1 - Proxy/InvocationHandler das chamadas individuais (para compreensão)
setDocumentLocator(any(Locator.class));
startDocument();
startElement(null, "ROOT", "ROOT", JsonSaxAdapter.EMPTY_ATTRIBUTES);
startElement(null, "id", "id", JsonSaxAdapter.STRING_ATTRIBUTES);
characters("1234567893".toCharArray(), 0, 10);
endElement(null, "id", "id");
startElement(null, "slots", "slots", JsonSaxAdapter.ARRAY_ATTRIBUTES);
startElement(null, "slots", "slots", JsonSaxAdapter.EMPTY_ATTRIBUTES);
startElement(null, "_type", "_type", JsonSaxAdapter.STRING_ATTRIBUTES);
characters("BannerSlot".toCharArray(), 0, 10);
endElement(null, "_type", "_type");
startElement(null, "id", "id", JsonSaxAdapter.STRING_ATTRIBUTES);
characters("1".toCharArray(), 0, 1);
endElement(null, "id", "id");
startElement(null, "interstitial", "interstitial", JsonSaxAdapter.BOOLEAN_ATTRIBUTES);
characters("false".toCharArray(), 0, 5);
...
endElement(null, "city", "city");
endElement(null, "location", "location");
endElement(null, "ROOT", "ROOT");
endDocument();
// Parte II - Uso dos próprios Mocks
verify(handler, times(1)).setDocumentLocator(any(Locator.class));
verify(handler, times(1)).startDocument();
verify(handler, times(1)).startElement(null, "ROOT", "ROOT", JsonSaxAdapter.EMPTY_ATTRIBUTES);
verify(handler, times(3)).startElement(null, "id", "id", JsonSaxAdapter.STRING_ATTRIBUTES);
verify(handler, times(1)).characters("1234567893".toCharArray(), 0, 10);
verify(handler, times(3)).endElement(null, "id", "id");
verify(handler, times(1)).startElement(null, "slots", "slots", JsonSaxAdapter.ARRAY_ATTRIBUTES);
// Linhas e linhas
verify(handler, times(1)).endElement(null, "city", "city");
verify(handler, times(1)).endElement(null, "location", "location");
verify(handler, times(1)).endElement(null, "ROOT", "ROOT");
verify(handler, times(1)).endDocument();
Process finished with exit code 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment