Created
July 3, 2014 09:47
-
-
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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