Last active
October 13, 2015 05:57
-
-
Save josefbetancourt/4149648 to your computer and use it in GitHub Desktop.
Very simple data file in Java source with embedded JUnit test. Presented in "Simple Java Data File" blog post.
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
/** Vsdf.java */ | |
package com.octodecillion.vsdf; | |
import static com.octodecillion.vsdf.Vsdf.EventType.END; | |
import static org.hamcrest.core.DescribedAs.describedAs; | |
import static org.hamcrest.core.Is.is; | |
import static org.hamcrest.core.IsEqual.equalTo; | |
import static org.hamcrest.core.IsNull.notNullValue; | |
import static org.junit.Assert.assertThat; | |
import static org.junit.Assert.assertTrue; | |
import java.io.BufferedReader; | |
import java.io.File; | |
import java.io.FileNotFoundException; | |
import java.io.FileReader; | |
import java.io.IOException; | |
import java.io.Reader; | |
import java.io.StringReader; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
import java.net.URL; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.Properties; | |
import java.util.concurrent.Callable; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
import javax.xml.parsers.DocumentBuilder; | |
import javax.xml.parsers.DocumentBuilderFactory; | |
import javax.xml.parsers.ParserConfigurationException; | |
import javax.xml.xpath.XPathConstants; | |
import javax.xml.xpath.XPathExpressionException; | |
import javax.xml.xpath.XPathFactory; | |
import org.junit.Before; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.junit.runners.JUnit4; | |
import org.w3c.dom.Document; | |
import org.w3c.dom.NodeList; | |
import org.xml.sax.InputSource; | |
import org.xml.sax.SAXException; | |
/** | |
* | |
* Very Simple Data File Support. | |
* | |
* An extension of INI files. More like external HEREDOCS | |
* with subdivisions. | |
* | |
* Written for Java 1.7 runtime. | |
* | |
* @see "http://octodecillion.com/blog/simple-java-data-file-in-java/" | |
* @see "http://octodecillion.com/blog/very-simple-data-file-format/" | |
* | |
* What is an alternative parsing approach? | |
* <a href="http://www.antlr4.org/">Antlr</a>, | |
* <a href="https://github.com/sirthias/parboiled/wiki">Parboiled</a> | |
* | |
* @author Josef Betancourt | |
* | |
*/ | |
public class Vsdf { | |
private BufferedReader reader; // when is it closed? | |
private Reader sourceReader; | |
private File file; | |
private int lineCounter; | |
private int sectionCounter; | |
private Event data; | |
private boolean reusable; | |
@SuppressWarnings("javadoc") | |
public enum EventType { | |
COMMENT, SECTION, EMPTY, END | |
} | |
@SuppressWarnings("javadoc") | |
public enum DataType { | |
TEXT, PROPERTIES, LIST, MAP, SET, DOCUMENT; | |
} | |
/** Default constructor */ | |
public Vsdf() { | |
} | |
/** | |
* | |
* @param inixData | |
* sectioned data | |
*/ | |
public Vsdf(final String inixData) { | |
sourceReader = new StringReader(inixData); | |
reader = new BufferedReader(sourceReader); | |
} | |
/** | |
* @param reader | |
*/ | |
public Vsdf(final Reader reader) { | |
this(); | |
if (reader == null) { | |
throw new NullPointerException("reader is null"); | |
} | |
sourceReader = reader; | |
this.reader = new BufferedReader(reader); | |
} | |
/** | |
* @param file | |
* @throws FileNotFoundException | |
*/ | |
public Vsdf(final File file) throws FileNotFoundException { | |
if (file == null) { | |
throw new NullPointerException("file is null"); | |
} | |
this.file = file; | |
sourceReader = new FileReader(file); | |
reader = new BufferedReader(sourceReader); | |
reusable = true; | |
} | |
/** | |
* If the input is a file, allow reuse of Vsdf instance. | |
* | |
* Side effect: If a file is being used, a new reader is created. | |
* | |
* @return true if reader will be recreated. | |
* @throws VsdfException | |
*/ | |
protected boolean reusable() { | |
if (!reusable) { | |
return false; | |
} | |
try { | |
reader = new BufferedReader(new FileReader(file)); | |
} catch (FileNotFoundException ex) { | |
throw new VsdfException(ex.getMessage(), ex); | |
} | |
return true; | |
} | |
/** | |
* Load a section's data. | |
* | |
* Example: | |
* | |
* <pre> | |
* String data = new Vsdf(getFile(TEST_FILE)).load(id); | |
* </pre> | |
* | |
* @param id | |
* @return data in section | |
* | |
* @throws IOException | |
*/ | |
public String load(final String id) throws IOException { | |
reusable(); | |
String data = ""; | |
EventType eventType; | |
while ((eventType = next()) != EventType.END) { | |
if ((eventType == EventType.SECTION) && match(id, "")) { | |
data = getEvent().text; | |
break; | |
} | |
} | |
reader.close(); | |
return data; | |
} | |
/** | |
* Load a section's data. | |
* | |
* Example: | |
* | |
* <pre> | |
* String data = new Vsdf(getFile(TEST_FILE)).load(id, subsection); | |
* </pre> | |
* | |
* @param id | |
* @param subsection | |
* @return data in section | |
* | |
* @throws IOException | |
*/ | |
public String load(final String id, final String subsection) | |
throws IOException { | |
if (isNullOrBlank(id)) { | |
throw new IllegalArgumentException(String.format( | |
"id, '%s' is null or blank", id)); | |
} | |
if (null == subsection) { | |
throw new IllegalArgumentException("subsection is null"); | |
} | |
reusable(); | |
String data = ""; | |
EventType eventType; | |
while ((eventType = next()) != EventType.END) { | |
if ((eventType == EventType.SECTION) && match(id, subsection)) { | |
data = getEvent().text; | |
break; | |
} | |
} | |
reader.close(); | |
return data; | |
} | |
/** | |
* See {@link #loadAs(DataType, String, String)} | |
*/ | |
@SuppressWarnings("javadoc") | |
public <T> T loadAs(final Class<T> type, final String id) | |
throws ClassCastException { | |
return loadAs(type, id, BLANK); | |
} | |
/** | |
* See {@link #loadAs(DataType, String, String)} | |
*/ | |
@SuppressWarnings("javadoc") | |
public Object loadAs(final DataType type, final String id) | |
throws ClassCastException { | |
return loadAs(type, id, BLANK); | |
} | |
/** | |
* See {@link #loadAs(DataType, String, String)} | |
*/ | |
@SuppressWarnings({ "unchecked", "javadoc" }) | |
public <T> T loadAs(final Class<T> type, final String id, | |
final String subsection) { | |
String typeName = type.getSimpleName().toUpperCase(Locale.ENGLISH); | |
DataType dType = DataType.valueOf(typeName); | |
return (T) loadAs(dType, id, subsection); | |
} | |
/** | |
* | |
* Load a section from inix file and parse according to the datatype. | |
* | |
* @param type | |
* @param id | |
* @param subsection | |
* @return parsed section data | |
* @returns the object or null | |
* @throws VsdfException | |
*/ | |
public Object loadAs(final DataType type, final String id, | |
final String subsection) { | |
reusable(); | |
Object data = null; | |
try { | |
String text = load(id, subsection); | |
if (!isNullOrBlank(text)) { | |
switch (type) { | |
case PROPERTIES: | |
data = new PropertiesConverter().parse(text); | |
break; | |
case LIST: | |
data = new ListConverter().parse(text); | |
break; | |
case DOCUMENT: | |
data = new DocumentConverter().parse(text); | |
break; | |
default: | |
break; | |
} | |
} | |
} catch (Exception ex) { | |
throw new VsdfException(ex.getMessage(), ex); | |
} | |
return data; | |
} | |
/** | |
* | |
* Add a new section to inix file; | |
* | |
* @param id | |
* @param subsection | |
* @param text | |
* @return true if section was added | |
* @throws IOException | |
*/ | |
public boolean addSection(final String id, final String subsection, | |
final String text) throws IOException { | |
reusable(); | |
boolean result = true; | |
boolean found = false; | |
if (!found) { | |
// TODO: write the section to end of file | |
// may have to write to temp file, etc. | |
} | |
return result; | |
} | |
private class ListConverter implements Converter { | |
public ListConverter() { | |
// | |
} | |
@SuppressWarnings("unchecked") | |
@Override | |
public List<String> parse(final String text) throws Exception { | |
return Arrays.asList(text.split(LINESEP)); | |
} | |
} | |
private class PropertiesConverter implements Converter { | |
public PropertiesConverter() { | |
// | |
} | |
@SuppressWarnings("unchecked") | |
@Override | |
public Properties parse(final String data) throws Exception { | |
Properties p = new Properties(); | |
p.load(new StringReader(data)); | |
return p; | |
} | |
} | |
private class DocumentConverter implements Converter { | |
public DocumentConverter() { | |
// | |
} | |
@SuppressWarnings("unchecked") | |
@Override | |
public Document parse(final String text) throws Exception { | |
return parseXML(text); | |
} | |
} | |
/** | |
* @author jbetancourt | |
* | |
*/ | |
public static interface Converter { | |
/** | |
* @param text | |
* @return the data converter to data type | |
* @throws Exception | |
*/ | |
public <T> T parse(String text) throws Exception; | |
} | |
/** | |
* Parse xml String. | |
* | |
* @param xml | |
* @return a Document | |
* @throws SAXException | |
* @throws IOException | |
* @throws ParserConfigurationException | |
*/ | |
Document parseXML(final String xml) throws SAXException, | |
IOException, ParserConfigurationException { | |
DocumentBuilder builder = DocumentBuilderFactory.newInstance() | |
.newDocumentBuilder(); | |
return builder.parse(new InputSource(new StringReader(xml))); | |
} | |
/** | |
* Read all the sections into a map. | |
* | |
* Just a convenience method, not recommended. | |
* | |
* Example: | |
* <code> | |
* Map<String, String> map = new Vsdf(getFile(TEST_FILE)).load(); | |
* </code> | |
* | |
* | |
* @return a map of section header string to section content as line | |
* separted string. | |
* @throws IOException | |
*/ | |
public Map<String, String> load() throws IOException { | |
reusable(); | |
Map<String, String> map = new HashMap<String, String>(); | |
EventType eventType; | |
while ((eventType = next()) != EventType.END) { | |
if (eventType == EventType.SECTION) { | |
Event ev = getEvent(); | |
map.put(ev.header, ev.text); | |
} | |
} | |
return map; | |
} | |
/** | |
* | |
* | |
* @return list of of array id, subsection. | |
* @throws IOException | |
*/ | |
public List<String[]> loadIndex() throws IOException { | |
reusable(); | |
List<String[]> list = new ArrayList<String[]>(); | |
EventType eventType; | |
while ((eventType = next()) != EventType.END) { | |
if (eventType == EventType.SECTION) { | |
Event ev = getEvent(); | |
String[] arr = new String[2]; | |
arr[0] = ev.id; | |
arr[1] = ev.subsection; | |
list.add(arr); | |
} | |
} | |
return list; | |
} | |
/** | |
* Read the data base and report on configuration. | |
* @return human readable report string | |
* | |
* @throws IOException | |
*/ | |
@SuppressWarnings("boxing") | |
public String testData() throws IOException { | |
reusable(); | |
EventType eventType; | |
int sectionCount = 0; | |
int blankSections = 0; | |
while ((eventType = next()) != EventType.END) { | |
if (eventType == EventType.SECTION) { | |
Event ev = getEvent(); | |
sectionCount++; | |
if((ev.text != null) && (ev.text.trim().length() == 0)) { | |
blankSections++; | |
} | |
} | |
} | |
return String.format("#sections=%d,#blank sections=%d", | |
sectionCount, blankSections); | |
} | |
/** | |
* Pull event style processing. | |
* | |
* @return event type | |
* @throws IOException | |
*/ | |
public EventType next() throws IOException { | |
if (reader == null) { | |
throw new NullPointerException(READER_IS_NULL); | |
} | |
String line = reader.readLine(); | |
lineCounter++; | |
data = new Event(); | |
EventType eventType = null; | |
data.lineNum = lineCounter; | |
if (line == null) { | |
eventType = EventType.END; | |
} else if (isBlank(line)) { | |
eventType = EventType.EMPTY; | |
} else if (line.matches(COMMENT_REGEX)) { | |
eventType = EventType.COMMENT; | |
data.text = line; | |
data.event = EventType.COMMENT; | |
data.lineNum = lineCounter; | |
} else if (line.matches(SECTION_START_REGEX)) { | |
eventType = EventType.SECTION; | |
sectionCounter++; | |
processSection(line.trim(), data); | |
} | |
return eventType; | |
} | |
/** | |
* Push event style processing. | |
* | |
* Read the file and push events via a runnable interface. | |
* | |
* @param reader | |
* BufferedReader | |
* @param callable | |
* @throws Exception | |
* | |
*/ | |
public <T> void parse(final BufferedReader reader, | |
final Callable<T> callable) throws Exception { | |
try { | |
setReader(reader); | |
while (next() != END) { | |
@SuppressWarnings("unused") | |
T result = callable.call(); | |
} | |
} finally { | |
reader.close(); | |
} | |
} | |
/** | |
* Push event style processing. | |
* | |
* See {@link #parse(BufferedReader, Callable)} | |
* | |
* @param filePath | |
* @param runnable | |
* @throws Exception | |
* | |
*/ | |
public void parse(final String filePath, final Runnable runnable) | |
throws Exception { | |
reader = new BufferedReader(new FileReader(new File(filePath))); | |
// Since parse requires a Callable, we wrap the runnable. yuck. | |
parse(reader, new Callable<Boolean>() { | |
@Override | |
public Boolean call() throws Exception { | |
runnable.run(); | |
return Boolean.TRUE; | |
} | |
}); | |
reader.close(); | |
} | |
/** | |
* Get info about the section. | |
* | |
* Fill in the Event data structure | |
* | |
* @param line | |
* @param dataEvent | |
* @throws IOException | |
*/ | |
private void processSection(final String line, final Event dataEvent) | |
throws IOException { | |
dataEvent.event = EventType.SECTION; | |
dataEvent.sectionNum = sectionCounter; | |
Matcher m; | |
if ((m = headerPattern.matcher(line)).find()) { | |
dataEvent.header = m.group(1); | |
} else { | |
throw new IOException("Could not extract header from: '" + line | |
+ "'"); | |
} | |
Matcher secMatcher = sectionStartPattern.matcher(line); | |
String mString = secMatcher.matches() ? secMatcher.group(1) : BLANK; | |
parseStartTag(dataEvent, mString.trim()); | |
dataEvent.text = readContent(); | |
} | |
/** | |
* Get the id and subsection from header string. | |
* | |
* @param dataEvent | |
* @param headerContent | |
* */ | |
private void parseStartTag(final Event dataEvent, final String headerContent) { | |
dataEvent.id = find(headerContent, idPattern); | |
if (isNullOrBlank(dataEvent.id)) { | |
throw new IllegalArgumentException(ID_MISSING + headerContent); | |
} | |
String type = find(headerContent, typePattern); | |
dataEvent.type = valueOf(type, DataType.TEXT.name()); | |
dataEvent.subsection = find(headerContent, subsectionPattern); | |
} | |
/** | |
* Find first matching group in string. | |
* | |
* @param current | |
* @param pattern | |
* @return string found or blank | |
* | |
*/ | |
private String find(final String current, final Pattern pattern) { | |
String found = ""; | |
Matcher matcher = pattern.matcher(current); | |
if (matcher.find()) { | |
found = matcher.group(1); | |
} | |
return found != null ? found : BLANK; | |
} | |
/** | |
* Read the data in the section. | |
* | |
* All lines will be included as data. | |
* | |
* @return the data as String | |
* @throws IOException | |
*/ | |
private String readContent() throws IOException { | |
StringBuilder buffer = new StringBuilder(8 * 1024); | |
while (true) { | |
String line = reader.readLine(); | |
lineCounter++; | |
if ((line == null) || line.trim().matches(TERMINAL_REGEX)) { | |
break; | |
} | |
if (line.trim().matches(ANY_SECTION_REGEX)) { | |
throw new IllegalStateException(UNTERMINATED_SECTION + " " | |
+ (data != null ? data.header : "")); | |
} | |
buffer.append(line + LINESEP); | |
} | |
return buffer.toString(); | |
} | |
/** | |
* Does current event id match. | |
* | |
* Invokes {@link #match(String, String)} with blank subsection. | |
* | |
* @param id | |
* @return true if match | |
*/ | |
public boolean match(final String id) { | |
return match(id, BLANK); | |
} | |
/** | |
* section match? | |
* | |
* @param id | |
* @param subsection | |
* @return true if match | |
*/ | |
public boolean match(final String id, final String subsection) { | |
if (isNullOrBlank(id)) { | |
throw new IllegalArgumentException(String.format( | |
ID_ARG_MATCH_ERROR, id)); | |
} | |
if (null == subsection) { | |
throw new IllegalArgumentException(String.format( | |
SUB_ARG_MATCH_ERROR, subsection)); | |
} | |
boolean idMatch = data.id.equals(id); | |
String sub = data.subsection == null ? BLANK : data.subsection; | |
boolean subMatch = sub.equals(subsection); | |
return idMatch && subMatch; | |
} | |
/** | |
* Event struct for parsed sections. | |
*/ | |
@SuppressWarnings("javadoc") | |
public class Event { | |
public EventType event; | |
public String type; | |
public String subsection; | |
public String id; | |
public String text; | |
public int lineNum; | |
public int sectionNum; | |
public String header; | |
@SuppressWarnings("boxing") | |
@Override | |
public String toString() { | |
return String.format(EVENT_TO_STRING, event, id, subsection, type, | |
header, lineNum); | |
} | |
} | |
/** | |
* Get current parse event. | |
* | |
* @return current event | |
*/ | |
public Event getEvent() { | |
return data; | |
} | |
/** | |
* @param reader | |
* the reader to set | |
*/ | |
public void setReader(final BufferedReader reader) { | |
if (reader == null) { | |
throw new NullPointerException("reader is null!"); | |
} | |
this.reader = reader; | |
} | |
/** | |
* @return the reader | |
*/ | |
public BufferedReader getReader() { | |
return reader; | |
} | |
/** | |
* Use string or default string. | |
* | |
* @param inString | |
* @param name | |
* @return inString or if blank or null, returns a blank | |
*/ | |
private String valueOf(final String inString, final String name) { | |
return isNullOrBlank(inString) ? name : inString; | |
} | |
/** | |
* Is string blank? | |
* | |
* @param obj | |
* @return true if so | |
*/ | |
private boolean isBlank(final String obj) { | |
if (obj == null) { | |
throw new NullPointerException("obj is null"); | |
} | |
return obj.trim().length() == 0; | |
} | |
/** | |
* Is object null, or string blank. | |
* | |
* @param obj | |
* @return true if it is | |
*/ | |
private boolean isNullOrBlank(final Object obj) { | |
if (obj == null) { | |
return true; | |
} | |
if (obj instanceof String) { | |
return ((String) obj).trim().length() == 0; | |
} | |
return false; | |
} | |
/** | |
* | |
* | |
* @author jbetancourt | |
* | |
*/ | |
public class VsdfException extends RuntimeException { | |
/** */ | |
private static final long serialVersionUID = 1L; | |
/** | |
* See {@link RuntimeException#RuntimeException()} | |
*/ | |
public VsdfException() { | |
super(); | |
} | |
/** | |
* See {@link RuntimeException#RuntimeException(String, Throwable) } | |
*/ | |
public VsdfException(final String message, final Throwable cause) { | |
super(message, cause); | |
} | |
/** | |
* See {@link RuntimeException#RuntimeException(String) }. | |
*/ | |
public VsdfException(final String message) { | |
super(message); | |
} | |
/** | |
* See {@link RuntimeException#RuntimeException(Throwable)}. | |
*/ | |
public VsdfException(final Throwable cause) { | |
super(cause); | |
} | |
} | |
private static final String BLANK = ""; | |
@SuppressWarnings("javadoc") | |
static final String LINESEP = System.getProperty("line.separator"); | |
private static final String EVENT_TO_STRING = "event=%s,id=%s, subsection=%s, type=%s, header=%s, lineNum=%s"; | |
private static final String READER_IS_NULL = "reader is null"; | |
private static final String UNTERMINATED_SECTION = "Unterminated section: "; | |
private static final String ID_ARG_MATCH_ERROR = "id arg to match is null or blank: '%s'"; | |
private static final String SUB_ARG_MATCH_ERROR = "subsection arg to match is null: '%s'"; | |
private static final String ID_MISSING = "id missing in section: "; | |
private static final String ANY_SECTION_REGEX = "^\\s*\\[>.*\\]\\s*"; | |
private static final String TERMINAL_REGEX = "^\\s*\\[<.*\\]\\s*"; | |
private static final String COMMENT_REGEX = "^\\s*[#!].*"; | |
private static final String HEADER_REGEX = "\\[>(.*)\\]\\s*"; | |
private static final Pattern headerPattern = Pattern.compile(HEADER_REGEX); | |
private static final String ID_REGEX = "^(?:.*:)?(.*?)(?:/.*)?$"; | |
private static final Pattern idPattern = Pattern.compile(ID_REGEX); | |
private static final String TYPE_REGEX = "^(.*):"; | |
private static final Pattern typePattern = Pattern.compile(TYPE_REGEX); | |
private static final String SECTION_START_REGEX = "^\\s*\\[>(.*)\\]\\s*"; | |
private static final Pattern sectionStartPattern = Pattern | |
.compile(SECTION_START_REGEX); | |
private static final String SUBSECTION_REGEX = "^(?:.*:)?.*?/(.*)$"; | |
private static final Pattern subsectionPattern = Pattern | |
.compile(SUBSECTION_REGEX); | |
@SuppressWarnings("boxing") | |
@Override | |
public String toString() { | |
return String.format("lineCounter=%s, sectionCounter=%s, " + "data=%s", | |
lineCounter, sectionCounter, data); | |
} | |
// ==================================================================== | |
// ==================================================================== | |
// Embedded JUnit test | |
// ==================================================================== | |
// ==================================================================== | |
/** | |
* Test {@link Vsdf}. | |
* | |
* @author Josef Betancourt | |
* | |
*/ | |
@RunWith(JUnit4.class) | |
public static class VsdfTest { | |
private static final String TEST_FILE = "TestDataTestFile.inix"; | |
/** | |
* the tested object. | |
*/ | |
protected Vsdf app; | |
/** | |
* @throws Exception | |
*/ | |
@Before | |
public void setUp() throws Exception { | |
app = new Vsdf(getFileInClassPath(TEST_FILE)); | |
} | |
/** | |
* @throws IOException | |
* | |
*/ | |
@SuppressWarnings("boxing") | |
@Test | |
public void should_loadIndex() throws IOException { | |
List<String[]> list = app.loadIndex(); | |
assertThat(list.size(), is(9)); | |
if (app.isReusable()) { | |
list = app.loadIndex(); | |
assertThat(list.size(), is(9)); | |
} | |
} | |
/** | |
* @throws IOException | |
* | |
*/ | |
@SuppressWarnings("boxing") | |
@Test | |
public void should_loadIndex_ofStringSrc() throws IOException { | |
Path path = Paths.get(getFileInClassPath(TEST_FILE) | |
.getAbsolutePath()); | |
String testData = new String(Files.readAllBytes(path)); | |
app = new Vsdf(testData); | |
List<String[]> list = app.loadIndex(); | |
assertThat("First attempt to loadIndex", list.size(), is(9)); | |
if (app.isReusable()) { | |
list = app.loadIndex(); | |
assertThat("Second attempt to loadIndex", list.size(), is(9)); | |
} | |
} | |
/** | |
* Test {@link "Vsdf#parseStartTag(Event, String)"}. | |
* | |
* @throws Exception | |
*/ | |
@Test | |
public void should_parseStartTag() throws Exception { | |
Method parseStartTag = getMethod("parseStartTag", Event.class, | |
String.class); | |
Vsdf vsdf = new Vsdf(); | |
Event event = vsdf.new Event(); | |
parseStartTag.invoke(vsdf, event, "x/y"); | |
assertThat(event.id, is(equalTo("x"))); | |
assertThat(event.subsection, is(equalTo("y"))); | |
} | |
/** | |
* @throws Exception | |
* @throws InvocationTargetException | |
* @throws IllegalAccessException | |
* @throws IllegalArgumentException | |
* | |
*/ | |
@Test | |
public void should_parseStartTag_fail() throws Exception { | |
try { | |
getMethod("parseStartTag", Event.class, String.class).invoke( | |
app, app.new Event(), ""); | |
} catch (Exception e) { | |
assertThat(e.getCause(), is(IllegalArgumentException.class)); | |
} | |
} | |
/** | |
* Test {@link Vsdf#load(String, String)}. | |
* | |
* @throws Exception | |
* @throws FileNotFoundException | |
* | |
*/ | |
@Test | |
public void shouldLoadSection() throws FileNotFoundException, Exception { | |
String actual = new Vsdf(getFileInClassPath(TEST_FILE)).load( | |
"credit", "alerts"); | |
String expected = "[\"one\",\"two\",\"three\"]" + LINESEP; | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@Test | |
public void shouldLoad_with_id() throws FileNotFoundException, | |
Exception { | |
String actual = new Vsdf(getFileInClassPath(TEST_FILE)) | |
.load("messages"); | |
String expected = "one=a one" + LINESEP + "two=a two" + LINESEP; | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@Test(expected = IllegalArgumentException.class) | |
public void shouldLoad_null_id() throws FileNotFoundException, | |
Exception { | |
new Vsdf(getFileInClassPath(TEST_FILE)).load(null, | |
"alerts"); | |
} | |
/** | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@Test(expected = IllegalArgumentException.class) | |
public void shouldLoad_null_Section() throws FileNotFoundException, | |
Exception { | |
new Vsdf(getFileInClassPath(TEST_FILE)).load( | |
"credit", null); | |
} | |
/** | |
* Test {@link Vsdf#loadAs(Class, String, String)}. | |
* | |
* @throws Exception | |
* @throws FileNotFoundException | |
* | |
*/ | |
@Test | |
public void shouldLoadProperties() throws FileNotFoundException, | |
Exception { | |
Properties actual = new Vsdf(getFileInClassPath(TEST_FILE)).loadAs( | |
Properties.class, "credit", "config"); | |
Properties expected = new Properties(); | |
expected.put("one", "alpha"); | |
expected.put("two", "beta"); | |
expected.put("three", "charlie"); | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* Test {@link Vsdf#loadAs(Class, String)}. | |
* | |
* @throws Exception | |
* @throws FileNotFoundException | |
* | |
*/ | |
@Test | |
public void shouldLoadProperties2() throws FileNotFoundException, | |
Exception { | |
Properties actual = new Vsdf(getFileInClassPath(TEST_FILE)).loadAs( | |
Properties.class, "messages"); | |
Properties expected = new Properties(); | |
expected.put("one", "a one"); | |
expected.put("two", "a two"); | |
assertThat(actual, | |
describedAs("messages properties", is(equalTo(expected)))); | |
} | |
/** | |
* Test {@link Vsdf#loadAs(DataType, String, String)}. | |
* | |
* @throws Exception | |
* @throws FileNotFoundException | |
* | |
*/ | |
@Test | |
public void shouldLoadPropertiesType() throws FileNotFoundException, | |
Exception { | |
Properties actual = (Properties) new Vsdf( | |
getFileInClassPath(TEST_FILE)).loadAs(DataType.PROPERTIES, | |
"credit", "config"); | |
Properties expected = new Properties(); | |
expected.put("one", "alpha"); | |
expected.put("two", "beta"); | |
expected.put("three", "charlie"); | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* | |
* Test {@link Vsdf#loadAs(Class, String)}. | |
* | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@SuppressWarnings("unchecked") | |
@Test | |
public void shouldLoadList() throws FileNotFoundException, Exception { | |
List<String> actual = new Vsdf(getFileInClassPath(TEST_FILE)) | |
.loadAs(List.class, "credit"); | |
List<String> expected = new ArrayList<String>(); | |
expected.add("one"); | |
expected.add("two"); | |
expected.add("three"); | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* Test {@link Vsdf#loadAs(DataType, String)}. | |
* | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@Test | |
public void shouldLoadListType() throws FileNotFoundException, | |
Exception { | |
@SuppressWarnings("unchecked") | |
List<String> actual = (List<String>) new Vsdf( | |
getFileInClassPath(TEST_FILE)).loadAs(DataType.LIST, | |
"credit"); | |
List<String> expected = new ArrayList<String>(); | |
expected.add("one"); | |
expected.add("two"); | |
expected.add("three"); | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* | |
* Test {@link Vsdf#loadAs(Class, String, String)}. | |
* | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@Test | |
public void shouldLoadXML() throws FileNotFoundException, Exception { | |
Document actualDom = new Vsdf(getFileInClassPath(TEST_FILE)) | |
.loadAs(Document.class, "credit", "beans"); | |
String actual = getItems(actualDom); | |
String xml = "<description><item>one</item><item>two</item><item>three</item></description>"; | |
Document expectedDom = (Document) invokeMethod("parseXML", | |
new Class<?>[] { String.class }, xml); | |
String expected = getItems(expectedDom); | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* Test {@link Vsdf#loadAs(DataType, String, String)}. | |
* | |
* @throws FileNotFoundException | |
* @throws Exception | |
*/ | |
@Test | |
public void shouldLoadXMLType() throws FileNotFoundException, Exception { | |
Document actualDom = (Document) new Vsdf( | |
getFileInClassPath(TEST_FILE)).loadAs(DataType.DOCUMENT, | |
"credit", "beans"); | |
String actual = getItems(actualDom); | |
String xml = "<description><item>one</item><item>two</item><item>three</item></description>"; | |
Document expectedDom = (Document) invokeMethod("parseXML", | |
new Class<?>[] { String.class }, xml); | |
String expected = getItems(expectedDom); | |
assertThat(actual, is(equalTo(expected))); | |
} | |
/** | |
* | |
*/ | |
@Test(expected = VsdfException.class) | |
public void should_fail_loadXML() { | |
String data = "[>first]\nabc\n[<]\n"; | |
app = new Vsdf(data); | |
app.loadAs(Document.class, "first"); | |
} | |
/** | |
* Get items elements text content and concatenates into String. | |
* | |
* @see {@link #shouldLoadXML()} | |
* | |
* @param actualDom | |
* @return concatenates items | |
* @throws XPathExpressionException | |
*/ | |
private String getItems(final Document actualDom) | |
throws XPathExpressionException { | |
NodeList nodes = (NodeList) XPathFactory.newInstance().newXPath() | |
.compile("/description/item") | |
.evaluate(actualDom, XPathConstants.NODESET); | |
StringBuffer buf = new StringBuffer(); | |
for (int i = 0; i < nodes.getLength(); i++) { | |
buf.append(nodes.item(i).getTextContent()); | |
} | |
String actual = buf.toString(); | |
return actual; | |
} | |
/** | |
* | |
* Test {@link Vsdf#load()}. | |
* | |
* @throws Exception | |
* @throws FileNotFoundException | |
* | |
*/ | |
@Test | |
public void shouldLoad() throws FileNotFoundException, Exception { | |
Map<String, String> map = new Vsdf(getFileInClassPath(TEST_FILE)) | |
.load(); | |
assertThat(map.get("credit/alerts"), notNullValue()); | |
assertThat(map.get("credit/report"), notNullValue()); | |
} | |
/** | |
* @throws IOException | |
*/ | |
@Test | |
public void should_testData() throws IOException { | |
String actual = app.testData(); | |
String expected = "#sections=9,#blank sections=1"; | |
assertThat(actual, is(expected)); | |
} | |
/** | |
* Test {@link Vsdf#next()}. | |
* | |
* @throws Exception | |
*/ | |
@Test | |
public void should_get_credit_sect2() throws Exception { | |
BufferedReader reader = new BufferedReader(new FileReader( | |
getFileInClassPath(TEST_FILE))); | |
try { | |
app.setReader(reader); | |
EventType ev; | |
while ((ev = app.next()) != END) { | |
if (ev == EventType.SECTION) { | |
if (app.match("credit")) { | |
break; | |
} | |
} | |
} | |
} finally { | |
reader.close(); | |
} | |
} | |
/** | |
* @throws Exception | |
* @throws InvocationTargetException | |
* @throws IllegalAccessException | |
* @throws IllegalArgumentException | |
* | |
*/ | |
@Test | |
public void should_fail_unterminated_section() throws Exception { | |
String data = "[>abc]\n123\n456"; | |
app.setReader(new BufferedReader(new StringReader(data))); | |
try { | |
getMethod("readContent").invoke(app); | |
} catch (Exception e) { | |
assertThat(e.getCause(), is(IllegalStateException.class)); | |
} | |
} | |
/** | |
* Test {@link Vsdf#next()}. | |
* | |
* @throws IOException | |
*/ | |
@Test(expected = NullPointerException.class) | |
public void should_not_parse_with_null_reader() throws IOException { | |
app.setReader(null); | |
app.next(); | |
} | |
/** | |
* | |
*/ | |
@Test(expected = IllegalArgumentException.class) | |
public void matchWithNull_fails() { | |
Vsdf v = new Vsdf(); | |
v.match(null); | |
} | |
/** | |
* | |
*/ | |
@Test(expected = IllegalArgumentException.class) | |
public void matchWithNull_null_fails() { | |
Vsdf v = new Vsdf(); | |
v.match(null, null); | |
} | |
/** | |
* | |
*/ | |
@Test(expected = NullPointerException.class) | |
public void setReader_with_null() { | |
app.setReader(null); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@SuppressWarnings("boxing") | |
@Test | |
public void valid_isNullOrBlank() throws Exception { | |
Vsdf vsdf = new Vsdf(); | |
Method method = getMethod("isNullOrBlank", Object.class); | |
boolean actual = (Boolean) method.invoke(vsdf, Boolean.TRUE); | |
assertThat(actual, is(false)); | |
} | |
/** | |
* | |
*/ | |
@Test | |
public void toString_event() { | |
Event ev = new Vsdf().new Event(); | |
ev.toString(); | |
} | |
/** | |
* | |
*/ | |
@Test | |
public void toString_Vsdf() { | |
app.toString(); | |
} | |
/** | |
* Test {@link Vsdf#parse(BufferedReader, Callable)}. | |
* | |
* @throws Exception | |
*/ | |
@SuppressWarnings("boxing") | |
@Test | |
public void should_get_credtest_sect() throws Exception { | |
@SuppressWarnings("resource") | |
BufferedReader reader = new BufferedReader(new FileReader( | |
getFileInClassPath(TEST_FILE))); | |
app.parse(reader, new Callable<Event>() { | |
@Override | |
public Event call() { | |
Event ev = app.getEvent(); | |
if (ev.event == EventType.SECTION) { | |
if (app.match("credit", "tests")) { | |
String[] data = ev.text.split(LINESEP); | |
assertThat(data.length, is(3)); | |
assertThat("one", is(data[0])); | |
assertThat("two", is(data[1])); | |
assertThat("three", is(data[2])); | |
} | |
} | |
return ev; | |
} | |
}); | |
} | |
/** | |
* Test {@link Vsdf#parse(BufferedReader, Callable)}. | |
* | |
* @throws Exception | |
*/ | |
@Test | |
public void should_get_credit_sect() throws Exception { | |
BufferedReader reader = new BufferedReader(new FileReader( | |
getFileInClassPath(TEST_FILE))); | |
app.parse(reader, new Callable<Event>() { | |
@SuppressWarnings("boxing") | |
@Override | |
public Event call() { | |
Event ev = app.getEvent(); | |
if (ev.event == EventType.SECTION) { | |
if (app.match("credit")) { | |
String[] data = ev.text.split(LINESEP); | |
assertThat(data.length, is(3)); | |
assertThat("one".compareTo(data[0]), is(0)); | |
assertThat("two".compareTo(data[1]), is(0)); | |
assertThat("three", is(equalTo(data[2]))); | |
assertThat("TEXT", is(equalTo(ev.type))); | |
} | |
} | |
return ev; | |
} | |
}); | |
reader.close(); | |
} | |
/** | |
* Test {@link Vsdf#parse(BufferedReader, Callable)}. | |
* | |
* @throws Exception | |
*/ | |
@Test | |
public void should_get_credit_sect_with_runnable() throws Exception { | |
BufferedReader reader = new BufferedReader(new FileReader( | |
getFileInClassPath(TEST_FILE))); | |
app.parse(getFileInClassPath(TEST_FILE).getAbsolutePath(), | |
new Runnable() { | |
@Override | |
@SuppressWarnings("boxing") | |
public void run() { | |
Event ev = app.getEvent(); | |
if (ev.event == EventType.SECTION) { | |
if (app.match("credit")) { | |
String[] data = ev.text.split(LINESEP); | |
assertThat(data.length, is(3)); | |
assertThat("one".compareTo(data[0]), is(0)); | |
assertThat("two".compareTo(data[1]), is(0)); | |
assertThat("three", is(equalTo(data[2]))); | |
assertThat("TEXT", is(equalTo(ev.type))); | |
} | |
} | |
} | |
}); | |
reader.close(); | |
} | |
/** | |
* Just for coverage completeness. | |
* | |
* @throws FileNotFoundException | |
*/ | |
@Test | |
public void constructWithFile() throws FileNotFoundException { | |
app = new Vsdf(getFileInClassPath(TEST_FILE)); | |
assertThat(app.getReader(), is(notNullValue())); | |
} | |
/** | |
* @throws FileNotFoundException | |
*/ | |
@Test(expected = NullPointerException.class) | |
public void constructWithFile_null() throws FileNotFoundException { | |
File file = null; | |
app = new Vsdf(file); | |
} | |
/** | |
* @throws FileNotFoundException | |
*/ | |
@Test(expected = FileNotFoundException.class) | |
public void constructWithFile_non_existent() | |
throws FileNotFoundException { | |
File file = new File("xyz\\abc\\zzz.123"); | |
app = new Vsdf(file); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@SuppressWarnings("resource") | |
@Test | |
public void constructWithReader() throws Exception { | |
app = new Vsdf(new FileReader(getFileInClassPath(TEST_FILE))); | |
assertThat(app.getReader(), is(notNullValue())); | |
Reader f = (Reader) getField("sourceReader"); | |
assertThat(f, is(notNullValue())); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test(expected = NullPointerException.class) | |
public void constructWithReader_null() throws Exception { | |
Reader r = null; | |
app = new Vsdf(r); | |
} | |
/** | |
* @throws FileNotFoundException | |
*/ | |
@Test | |
public void constructWithString() throws FileNotFoundException { | |
Vsdf v = new Vsdf("[>x]abc[<]"); | |
assertThat(v.getReader(), is(notNullValue())); | |
} | |
/** | |
* @throws FileNotFoundException | |
*/ | |
@Test(expected = NullPointerException.class) | |
public void constructWithString_null() throws FileNotFoundException { | |
String s = null; | |
new Vsdf(s); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test | |
public final void reg1() throws Exception { | |
regTest("list:credit/tests", "typePattern", "list"); | |
regTest("list:credit/tests", "idPattern", "credit"); | |
regTest("list:credit/tests", "subsectionPattern", "tests"); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test | |
public final void reg2() throws Exception { | |
regTest("map:/junk", "typePattern", "map"); | |
regTest("map:/junk", "idPattern", BLANK); | |
regTest("map:/junk", "subsectionPattern", "junk"); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test | |
public final void reg3() throws Exception { | |
regTest("list:credit", "typePattern", "list"); | |
regTest("list:credit", "idPattern", "credit"); | |
regTest("list:credit", "subsectionPattern", BLANK); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test | |
public final void reg4() throws Exception { | |
regTest("credit", "typePattern", BLANK); | |
regTest("credit", "idPattern", "credit"); | |
regTest("credit", "subsectionPattern", BLANK); | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test | |
public final void reg5() throws Exception { | |
regTest(BLANK, "idPattern", BLANK); | |
} | |
/** | |
* Invoke private method to test regex. | |
* | |
* @param line | |
* @param patternName | |
* @param expected | |
* @throws Exception | |
* | |
*/ | |
private void regTest(final String line, final String patternName, | |
final String expected) throws Exception { | |
Pattern pattern = (Pattern) getField(patternName); | |
String actual = (String) invokeMethod("find", new Class<?>[] { | |
String.class, Pattern.class }, line, pattern); | |
assertTrue(String.format("%s failed for input: %s, pattern=%s", | |
patternName, line, pattern), | |
actual.compareTo(expected) == 0); | |
} | |
/** | |
* Test {@link "Vsdf#isBlank(String)"}. | |
* | |
* @throws Exception | |
*/ | |
@SuppressWarnings("boxing") | |
@Test | |
public final void testIsBlank() throws Exception { | |
Vsdf vsdf = new Vsdf(); | |
Method method = getMethod("isBlank", String.class); | |
boolean actual = (Boolean) method.invoke(vsdf, ""); | |
assertThat(actual, is(true)); | |
actual = (Boolean) method.invoke(vsdf, " "); | |
assertThat(actual, is(true)); | |
actual = (Boolean) method.invoke(vsdf, "x"); | |
assertThat(actual, is(false)); | |
try { | |
actual = (Boolean) method.invoke(vsdf, new Object[] { null }); | |
} catch (InvocationTargetException e) { | |
Throwable cause = e.getCause(); | |
assertThat(cause, is(NullPointerException.class)); | |
} | |
} | |
/** | |
* @throws Exception | |
*/ | |
@Test | |
public void processSection_with_null() throws Exception { | |
Method method = getMethod("processSection", String.class, | |
Event.class); | |
String line = "xxxx"; | |
Event event = app.new Event(); | |
try { | |
method.invoke(app, line, event); | |
} catch (Exception e) { | |
Throwable cause = e.getCause(); | |
assertThat(cause, is(IOException.class)); | |
} | |
} | |
/** | |
* Test {@link Vsdf#isNullOrBlank(Object)}. | |
* | |
* @throws Exception | |
*/ | |
@SuppressWarnings({ "boxing", "javadoc" }) | |
@Test | |
public final void testIsNullOrBlank() throws Exception { | |
Vsdf vsdf = new Vsdf(); | |
Method method = getMethod("isNullOrBlank", Object.class); | |
boolean actual = (Boolean) method.invoke(vsdf, ""); | |
assertThat(actual, is(true)); | |
actual = (Boolean) method.invoke(vsdf, " "); | |
assertThat(actual, is(true)); | |
actual = (Boolean) method.invoke(vsdf, (Object) null); | |
assertThat(actual, is(true)); | |
actual = (Boolean) method.invoke(vsdf, "x"); | |
assertThat(actual, is(false)); | |
} | |
/** | |
* | |
* Get method and invoke it. | |
* | |
* @author jbetancourt | |
* | |
* @param name | |
* @param types | |
* @param args | |
* @return | |
* @throws Exception | |
*/ | |
private final Object invokeMethod(final String name, | |
final Class<?>[] types, final Object... args) throws Exception { | |
return getMethod(name, types).invoke(app, args); | |
} | |
/** | |
* Get private method util. | |
* | |
* @param name | |
* @param parameterTypes | |
* @return the method | |
* @throws Exception | |
* | |
* | |
*/ | |
private final Method getMethod(final String name, | |
final Class<?>... parameterTypes) throws Exception { | |
Method method = Vsdf.class.getDeclaredMethod(name, parameterTypes); | |
method.setAccessible(true); | |
return method; | |
} | |
/** | |
* Get field by reflection. | |
* | |
* @param name | |
* @return the value of the field. | |
* @throws Exception | |
* | |
*/ | |
private Object getField(final String name) throws Exception { | |
Field field = app.getClass().getDeclaredField(name); | |
field.setAccessible(true); | |
return field.get(app); | |
} | |
/** | |
* | |
* Get File in classpath. | |
* | |
* @param fileName | |
* @return the file | |
* @throws FileNotFoundException | |
*/ | |
private File getFileInClassPath(final String fileName) | |
throws FileNotFoundException { | |
URL url = this.getClass().getResource(fileName); | |
if (url == null) { | |
throw new FileNotFoundException("Resource not found: '" | |
+ fileName + "' "); | |
} | |
return new File(url.getFile()); | |
} | |
} | |
/** | |
* @return the reusable | |
*/ | |
public boolean isReusable() { | |
return reusable; | |
} | |
// Test file TestDataFile.inix contains: | |
/* @formatter:off | |
# Example very simple data file | |
! Another comment | |
#[>map:/junk] | |
#[<] | |
[>list:credit/tests] | |
one | |
two | |
three | |
[<] | |
[>credit/report] | |
one,two,three | |
[<] | |
[>properties:credit/config] | |
one=alpha | |
two=beta | |
three=charlie | |
[<] | |
[>xml:credit/beans] | |
<description> | |
<item>one</item> | |
<item>two</item> | |
<item>three</item> | |
</description> | |
[<] | |
[>notype/sub] | |
[<] | |
[>credit/alerts] | |
["one","two","three"] | |
[<] | |
[>credit] | |
one | |
two | |
three | |
[<] | |
[>credit/coverages] | |
["one","two","three"] | |
[<] | |
[>messages] | |
one=a one | |
two=a two | |
[<messages] | |
*/ | |
} // end of class Vsdf |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Code to accompany blog post: http://octodecillion.com/blog/simple-java-data-file-in-java/