Last active
December 29, 2015 17:09
-
-
Save josefbetancourt/7701645 to your computer and use it in GitHub Desktop.
Inix file reader source code to accompany blog post "Groovy implementation of INIX file format"
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
# Example very simple data file | |
# | |
[>root] | |
one | |
two | |
three | |
[<root] | |
[>demo1/compile] | |
x,y,z | |
[<demo1/compile] | |
[>demo1/deploy?skip=true&end=no] | |
x,y,z | |
[<demo1/deploy] | |
[>hook/root/compile?when=finished&skip=false] | |
a,b,c | |
[<] | |
[>demo1/deploy#two?skip=true&end=no] | |
x,y,z | |
[<demo1/deploy#two] | |
[>test8/@demo1/deploy] | |
Orignal content | |
[<] | |
[>unmatched] | |
a=1 | |
b=2 | |
[<notmatched] |
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
package com.octodecillion.util.inix | |
import groovy.transform.TypeChecked; | |
import groovy.transform.TypeCheckingMode | |
import java.io.IOException; | |
import java.util.regex.Pattern | |
import java.util.regex.Matcher | |
import org.codehaus.groovy.ast.stmt.CatchStatement | |
import groovy.transform.TypeChecked | |
/** | |
* | |
* Inix file support.<P> | |
* An inix file is an 'ini' like format. | |
* | |
* <pre> | |
* # a comment | |
* ; another comment | |
* [>sectionPath#fragment@alias?arguments] | |
* [<] or [<sectionPath] | |
* </pre> | |
* | |
* Example section: | |
* <pre> | |
* [>demo1/deploy#two@account897@policy253?enabled=true&owner=false] | |
* x,y,z | |
* [<demo1/deploy#two] | |
* </pre> | |
* | |
* Not thread safe.<p> | |
* | |
* @author Josef Betancourt | |
* | |
*/ | |
@TypeChecked | |
class Inix { | |
BufferedReader reader // need readLine | |
int lineNum | |
int sectionNum | |
Event ev | |
Map<String,String> sections = [:] | |
def Inix(){ | |
} | |
/** | |
* | |
* TODO should a Reader be used as param instead. | |
*/ | |
def Inix(BufferedReader r){ | |
checkNotNull(r, "reader is null") | |
reader = r | |
this | |
} | |
def Inix(String path){ | |
checkNotNull(path, "path is null") | |
setPath(path) | |
} | |
Inix setPath(String path){ | |
checkNotNull(path, "path is null") | |
reader = new BufferedReader( | |
new FileReader(new File(path))) | |
this | |
} | |
def close() throws IOException { | |
if(reader != null){ | |
reader.close() | |
} | |
} | |
/** | |
* Load all sections. | |
* | |
* @return map of section name to contents as String | |
*/ | |
Map<String,String> load(){ | |
EventType eventType | |
while( (eventType = next()) != EventType.END){ | |
Event ev = getEvent() | |
if(event && (eventType = EventType.SECTION)){ | |
sections.put(ev.sectionString, ev.text) | |
println "${ev.sectionString}" | |
} | |
} | |
sections | |
} | |
/** | |
* | |
* @param path | |
* @param fragment | |
* @return | |
*/ | |
String load(String path, String fragment){ | |
checkNotNull(path, "path is null") | |
checkNotNull(fragment, "fragment is null") | |
def result = BLANK | |
EventType eventType | |
while( (eventType = next()) != EventType.END){ | |
if( event && (eventType = EventType.SECTION) && match(getEvent(),path)){ | |
Event ev = getEvent() | |
result = ev.text | |
break | |
} | |
} | |
if(result.length() == 0){ | |
throw new IOException(String.format("Did not locate path", path)); | |
} | |
return result | |
} | |
String load(String path){ | |
load(path.split('/')) | |
} | |
String load(String... path){ | |
checkNotNull(path, "path is null") | |
def result = BLANK | |
EventType eventType | |
while( (eventType = next()) != EventType.END){ | |
if( event && (eventType = EventType.SECTION) && match(getEvent(),path)){ | |
Event ev = getEvent() | |
result = ev.text | |
break | |
} | |
} | |
if(result.length() == 0){ | |
throw new IOException("Did not locate path: $path"); | |
} | |
return result | |
} | |
/** | |
* Pull event style processing. | |
* | |
* @return event structure | |
*/ | |
EventType next(){ | |
def eventType | |
try{ | |
String line = reader.readLine() | |
if(line == null){ | |
reader.close() | |
return EventType.END | |
} | |
lineNum++ | |
line = line.trim() | |
Event ev = new Event() | |
if( isBlank(line)){ | |
return EventType.BLANK | |
}else if(isComment(line)){ | |
ev.text = line | |
ev.event = EventType.COMMENT | |
ev.lineNum = lineNum | |
eventType = EventType.COMMENT | |
}else if(isSection(line)){ // section? | |
ev.event = eventType = EventType.SECTION | |
ev.sectionString = line | |
ev.sectionNum = sectionNum++ | |
def args = parseSectionTag(ev,line) | |
if(args){ | |
parseParams(ev, args) | |
} | |
ev.text = readSectionData(ev) | |
} | |
this.ev = ev | |
}catch(IOException ex){ | |
reader.close(); | |
ex.printStackTrace(); | |
throw ex; | |
} | |
return eventType | |
} // end class Event | |
public enum EventType { | |
INIT, COMMENT, BLANK, SECTION, END, UNKNOWN | |
} | |
Event getEvent(){ | |
return ev | |
} | |
def String toString() { | |
return "event: $ev; sectionNum: $sectionNum; lineNum: $lineNum" | |
} | |
/** | |
* Parse and return any args | |
* | |
* @return args string | |
*/ | |
@TypeChecked(TypeCheckingMode.SKIP) | |
private String parseSectionTag(Event ev, String line){ | |
Matcher matcher = (line =~ SECTPAT) // [>.....] | |
if(!matcher){ | |
throw new IllegalArgumentException("$NOTPARSELINE: $line") | |
} | |
//String ws= (((List)matcher[0])[1]).trim() | |
String ws= (matcher[0][1]).trim() | |
if(!ws){ | |
throw new IllegalArgumentException("section $sectionNum is blank") | |
} | |
ev.tagString = parseTagString(line) | |
// get @xxx@xxx aliases that come before '?' | |
matcher = (ev.tagString =~ /(@.*)(?:\?.*|)/) | |
if(matcher){ | |
def ws1 = (matcher[0])[1].trim() | |
def wl = ws1.split("@") as List | |
ev.alias = wl.subList(1, wl.size()) | |
} | |
def wl = ws.split(/\?/) | |
def args = '' | |
if(wl.size()>1){ | |
args = wl[1] | |
} | |
String[] sArray = wl[0].split("#") | |
ev.path= sArray[0].split('/') as List | |
if(sArray.size()>1){ | |
ev.fragment = sArray[1] | |
} | |
args | |
} | |
private parseTagString(String line){ | |
def found = "" | |
def m = (line =~ /^\[>(.*?)(?:\?.*\]|\]$)/) | |
if(m){ | |
List matches = (List)m[0] | |
found = matches[1] | |
} | |
found | |
} | |
private parseParams(Event event, String args) { | |
args.split('&').each{ String s -> | |
def parts = s.split('=') | |
if(parts.length < 2){ | |
throw new IllegalArgumentException("Malformed args: $args") | |
} | |
event.addParam(parts[0].trim(),parts[1].trim()) | |
} | |
} | |
/** | |
* Get data in section. | |
* | |
* @return data as string | |
*/ | |
private String readSectionData(Event ev){ | |
StringBuilder buffer = new StringBuilder(INITBUFSIZE) | |
buffer.append("") | |
while(true){ | |
String line = reader.readLine() | |
lineNum++ | |
if(line == null){ | |
break | |
} | |
line = line.trim() | |
if(isEnd(line)){ | |
String endTag = parseEndTag(line) | |
if(endTag?.size()>1){ | |
if(endTag != ev.tagString){ | |
throw new IllegalArgumentException("Wrong end termination for ${endTag}") | |
} | |
} | |
break | |
}else if(isSection(line)){ | |
throw new IllegalArgumentException(UNTERMINATED) | |
}else{ | |
buffer.append(line + LINESEP) | |
} | |
} | |
return buffer.toString() | |
} | |
private String parseEndTag(String line){ | |
def m = (line =~ /^\[<(.*?)(?:\?.*\]|\]$)/) | |
if(!m){ | |
throw new IllegalArgumentException("$NOTPARSELINE: $line") | |
} | |
List matches = (List)m[0] | |
matches.size()>1 ? matches[1] : "" | |
} | |
private boolean match(Event ev, String id, String subsection){ | |
checkNotNull(id, "'id' is null") | |
checkNotNull(subsection, "subsection is null") | |
if(!ev.path.size()){ | |
return false | |
} | |
List targetPath = [id] + subsection.split('/').toList() | |
match(ev,targetPath) | |
} | |
private checkNotNull(obj, String message){ | |
if(!obj){ | |
throw new NullPointerException(message) | |
} | |
} | |
@TypeChecked | |
private boolean match(Event ev, String path){ | |
match(ev, path.split('/')) | |
} | |
@TypeChecked | |
private boolean match(Event ev, List path){ | |
if(ev.path.size() != path.size()){ | |
return false | |
} | |
def ith = 0 | |
ev.path.each{ p -> | |
if(p != path[ith++]){ | |
return false | |
} | |
} | |
true | |
} | |
@TypeChecked | |
private boolean match(Event ev, String[] path){ | |
if(ev.path.size() != path.size()){ | |
return false | |
} | |
match(ev, path.toList()) | |
} | |
/** | |
* Value object for parsed sections. | |
*/ | |
@TypeChecked | |
def class Event { | |
EventType event = EventType.INIT | |
List<String> path = [] | |
String fragment ='' | |
String text = '' | |
int lineNum | |
int sectionNum | |
Map<String,String> params = [:] | |
String sectionString = '' | |
String tagString | |
List<String> alias = [] | |
Map<String,String>args = [:] | |
Map addParam(String key, String value){ | |
params[key] = value | |
return params | |
} | |
boolean isSection(){ | |
event == Inix.EventType.SECTION | |
} | |
boolean isSection(String p){ | |
isSection() && compare(p) | |
} | |
boolean isSection(List<String> p){ | |
isSection() && compare(p) | |
} | |
boolean isSection(String p, String f){ | |
isSection() && compare(p,f) | |
} | |
boolean isSection(List<String> p, String f){ | |
isSection() && compare(p,f) | |
} | |
boolean compare(String p){ | |
return compare(p,"") | |
} | |
boolean compare(List<String> p){ | |
return compare(p, "") | |
} | |
boolean compare(String p, String f){ | |
return path.join('/').compareTo(p)==0 && fragment.compareTo(f)==0 | |
} | |
boolean compare(List<String> p, String f){ | |
return path.equals(p) && fragment.compareTo(f)==0 | |
} | |
String toString() { | |
def p = (params.collect{key,value -> "$key=$value"}).join(',') | |
return "path=[$path],fragment=[$fragment],params=[$p],lineNum=[$lineNum],eventType=[$event],sectionString=[$sectionString]" | |
} | |
} | |
private isSection(line){ | |
return line =~ /^\[>.*\]/ | |
} | |
private isEnd(line){ | |
return line =~ /^\[<.*\]/ || line =~ /^\[>.*\]/ | |
} | |
private isBlank(String s) { | |
return (s == null ? false : (s.trim().length() ==0 ? true : false)) | |
} | |
private isComment(line) { | |
return line =~ COMMENTPAT | |
} | |
private static final String BLANK = "" | |
static final String LINESEP = System.getProperty("line.separator") | |
static final String SECTPAT = /^\[>(.*)\]/ | |
static final String ENDPAT = /^\[>(.*?)(?:\?.*\]|\]$)/ | |
static final String COMMENTPAT = /^\s*[#;]/ | |
static final String NOTPARSELINE = "Could not parse line" | |
static final String UNTERMINATED = "Unterminated section" | |
static final int INITBUFSIZE = 8*1024 | |
} // end of class Inix |
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
package com.octodecillion.util.inix | |
import java.util.List; | |
import java.util.Map; | |
import java.util.regex.Matcher | |
import org.junit.After; | |
import org.junit.Before; | |
import org.junit.Ignore; | |
import org.junit.Test; | |
import org.junit.internal.InexactComparisonCriteria; | |
import org.junit.rules.ExpectedException; | |
import com.octodecillion.util.inix.Inix.Event | |
import com.octodecillion.util.inix.Inix.EventType; | |
import static com.octodecillion.util.inix.Inix.EventType.END | |
/** | |
* Test Inix.groovy class. | |
* @author J. Betancourt | |
* | |
*/ | |
class InixTest { | |
def LINESEP = System.properties.get("line.separator") | |
def BASEPATH = "src/test/resources" | |
def TESTDATA = "$BASEPATH/Data1.inix" | |
def inix; | |
def counter = new Counter() | |
def FULLREGEX = /^\[>(.*?)(#.*?)?(@.*?)?(\?.*?)?\]$/ | |
@Before | |
void before() { | |
inix = new Inix() | |
} | |
@After | |
void after() { | |
inix.close() | |
} | |
@Test | |
void shouldParseWholeTagString() { | |
Matcher m = ("[>ABC/DEF#GHI@XYZ?J=1&K=2]" =~ FULLREGEX) | |
if(m){ | |
List matches = m[0] | |
assert 5 == matches.size() | |
assert 'ABC/DEF' == matches[1] | |
assert '#GHI' == matches[2] | |
assert '@XYZ' == matches[3] | |
assert '?J=1&K=2' == matches[4] | |
} | |
} | |
@Test | |
void shouldParseWholeTagString2() { | |
Matcher m = ("[>ABC/DEF#GHI?J=1&K=2]" =~ FULLREGEX) | |
if(m){ | |
List matches = m[0] | |
assert 5 == matches.size() | |
assert 'ABC/DEF' == matches[1] | |
assert '#GHI' == matches[2] | |
assert null == matches[3] | |
assert '?J=1&K=2' == matches[4] | |
} | |
} | |
@Test | |
void shouldParseWholeTagString3() { | |
Matcher m = ("[>ABC/DEF]" =~ FULLREGEX) | |
if(m){ | |
List matches = m[0] | |
assert 5 == matches.size() | |
assert 'ABC/DEF' == matches[1] | |
assert null == matches[2] | |
assert null == matches[3] | |
assert null == matches[4] | |
} | |
} | |
@Test | |
void shouldParseWholeTagString4() { | |
Matcher m = ("[>ABC/DEF#DEF]" =~ FULLREGEX) | |
if(m){ | |
List matches = m[0] | |
assert 5 == matches.size() | |
assert 'ABC/DEF' == matches[1] | |
assert '#DEF' == matches[2] | |
assert null == matches[3] | |
assert null == matches[4] | |
} | |
} | |
@Test | |
void shouldParseWholeTagString5() { | |
Matcher m = ("[>ABC/DEF#DEF@abc@def]" =~ FULLREGEX) | |
if(m){ | |
assert 4 == m.groupCount() | |
List matches = m[0] | |
assert 'ABC/DEF' == matches[1] | |
assert '#DEF' == matches[2] | |
assert '@abc@def' == matches[3] | |
assert null == matches[4] | |
} | |
} | |
@Test | |
void shouldParseWholeTagString6() { | |
def pattern = ~/(@.*?)(?:@.*?)/ | |
Matcher regexMatcher = pattern.matcher("@abc@def"); | |
while (regexMatcher.find()) { | |
println regexMatcher.group() | |
println regexMatcher.start() | |
println regexMatcher.end() | |
} | |
} | |
@Test | |
void shouldParseTagString() { | |
def s = inix.parseTagString("[>ABC/DEF#GHI?J=1&K=2]") | |
assert s == "ABC/DEF#GHI" | |
} | |
@Test | |
void parseSection1(){ | |
def ev = new Inix.Event() | |
def args = inix.parseSectionTag(ev, "[>demo1/deploy?skip=true&end=no]") | |
assert "skip=true&end=no" == args | |
} | |
@Test | |
void parseSection2(){ | |
def ev = new Inix.Event() | |
def args= inix.parseSectionTag(ev,"[>hook/root/compile?when=finished&skip=false]") | |
assert "when=finished&skip=false" == args | |
} | |
@Test | |
void parseSection3(){ | |
def ev = new Inix.Event() | |
def args= inix.parseSectionTag(ev,"[>hook/root/compile#two?when=finished&skip=false]") | |
assert "when=finished&skip=false" == args | |
} | |
@Test | |
void parseSectionWithAlias(){ | |
def ev = new Inix.Event() | |
def args= inix.parseSectionTag(ev,"[>hook/root/compile@UVW@XYZ?when=finished&skip=false]") | |
//assert "when=finished&skip=false" == args | |
assert ev.alias[0] == 'UVW' | |
assert ev.alias[1] == 'XYZ' | |
} | |
@Test | |
void parseSectionWithAlias2(){ | |
def ev = new Inix.Event() | |
def args= inix.parseSectionTag(ev,"[>hook/root/compile#two@XYZ?when=finished&skip=false]") | |
assert "when=finished&skip=false" == args | |
assert ev.alias[0] == 'XYZ' | |
} | |
@Test | |
void shouldLoadSection(){ | |
def actual = inix.setPath(TESTDATA).load('hook','root','compile') | |
def expected ='a,b,c' + Inix.LINESEP | |
assert actual == expected | |
} | |
@Test | |
void shouldLoadAll(){ | |
def expected = 'notmatched' | |
try { | |
assert (inix.setPath(TESTDATA).load()).size() == 8; | |
} catch (IllegalArgumentException e) { | |
assert e.getMessage() == "Wrong end termination for ${expected}" | |
} | |
assert inix.sections.size() == 7 | |
} | |
@Test | |
void shouldViaVariableArgs(){ | |
inix.setPath(TESTDATA) | |
def theEvent = inix.next() | |
def found = false | |
while(theEvent != END){ | |
def event = inix.getEvent() | |
if(event && event.isSection(['hook', 'root', 'compile'])){ | |
found = true | |
assert event.text.size() == 7 | |
break | |
} | |
theEvent = inix.next() | |
} | |
assert found | |
} | |
@Test | |
void testshouldGetListData(){ | |
inix.setPath(TESTDATA) | |
def theEvent = inix.next() | |
def found = false | |
while(theEvent != END){ | |
def event = inix.getEvent() | |
if(event && event.isSection(['root'])){ | |
found = true | |
def data = event.text.split(LINESEP) | |
assert data.size() == 3 | |
assert ( (data[0] == 'one') && | |
(data[1] == 'two') && | |
(data[2] == 'three') ) | |
break | |
} | |
theEvent = inix.next() | |
} | |
assert found | |
} | |
@Test | |
void shouldGetSectionDemo1Compile(){ | |
inix.setPath(TESTDATA) | |
EventType theEvent = inix.next() | |
def found = false | |
while(theEvent != END){ | |
def event = inix.getEvent() | |
if(event && event.isSection(['demo1', 'compile'])){ | |
counter.increment() | |
found = true | |
def actual = 'x,y,z'+LINESEP | |
assert event.text == actual | |
break | |
} | |
theEvent = inix.next() | |
} | |
assert found | |
counter.assertCount(1) | |
} | |
@Test | |
void shouldGetSectionDemo1DeployWithArgs(){ | |
inix.setPath(TESTDATA) | |
def theEvent = inix.next() | |
def found = false | |
while(theEvent != END){ | |
def event = inix.getEvent() | |
if(event && event.isSection(['demo1', 'deploy'])){ | |
found = true | |
def actual = 'x,y,z'+LINESEP | |
assert event.text == actual | |
actual = event.params['skip'] | |
assert "true".compareTo(actual) == 0 | |
assert 'no' == event.params['end'] | |
break | |
} | |
theEvent = inix.next() | |
} | |
assert found | |
} | |
@Test | |
void shouldGetSectionDemo1DeployWithArgsAndFragment(){ | |
inix.setPath(TESTDATA) | |
def theEvent = inix.next() | |
def found = false | |
while(theEvent != END){ | |
def event = inix.getEvent() | |
if(event && event.isSection(['demo1', 'deploy'],'two')){ | |
found = true | |
def actual = 'x,y,z'+LINESEP | |
assert event.text == actual | |
actual = event.params['skip'] | |
assert "true".compareTo(actual) == 0 | |
assert 'no' == event.params['end'] | |
assert 'two' == event.fragment | |
break | |
} | |
theEvent = inix.next() | |
} | |
assert found | |
} | |
@Test | |
void testSplit() { | |
def ar = "abc".split("&") | |
println ar | |
} | |
@Test(expected=IllegalArgumentException.class) | |
void parseCommaOnlyParams(){ | |
def ev = new Event() | |
inix.parseParams(ev, ',,') | |
assert ev.params.size() == 0 | |
} | |
@Test(expected=IllegalArgumentException) | |
void parseMallFormedParams(){ | |
def ev = new Event() | |
inix.parseParams(ev, '=') | |
} | |
@Test(expected=IllegalArgumentException) | |
void parseEmptyParams(){ | |
def ev = new Event() | |
inix.parseParams(ev, '') | |
} | |
@Test(expected=IllegalArgumentException) | |
void testBadSectionHeader(){ | |
def ev = new Event() | |
def line = "[>]" | |
inix.parseSectionTag(ev,line) | |
} | |
@Test | |
void testEventToString(){ | |
def ev = new Inix.Event() | |
ev.event= Inix.EventType.SECTION | |
ev.path= ['a', 'b', 'c'] | |
ev.text = "how now" | |
ev.lineNum = 22 | |
ev.sectionNum= 4 | |
ev.params = ([one:'1',two:'2']) | |
ev.sectionString = "a/b/c?one=1&two=2" | |
assert "path=[[a, b, c]],fragment=[],params=[one=1,two=2],lineNum=[22],eventType=[SECTION],sectionString=[a/b/c?one=1&two=2]" == | |
ev.toString() | |
} | |
@Test | |
void compareEvent(){ | |
def ev = new Inix.Event() | |
ev.path= ['a', 'b', 'c'] | |
ev.params = ([one:'1',two:'2']) | |
def list = ['a', 'b', 'c'] | |
def actual = ev.compare(list,'') | |
assert true == actual | |
} | |
@Test | |
void compareEventWithStringPath(){ | |
Event ev = new Inix.Event() | |
ev.path= ['a', 'b', 'c'] | |
ev.params = ([one:'1',two:'2']) | |
def actual = ev.compare("a/b/c", '') | |
assert true == actual | |
} | |
@Test | |
void parseParams(){ | |
def ev = new Event() | |
inix.parseParams(ev, 'a=1&b=2') | |
assert ev.params == [a:'1',b:'2'] | |
} | |
@Test(expected=IllegalArgumentException) | |
void parseBadParams(){ | |
def ev = new Event() | |
inix.parseParams(ev, 'a=1&b=') | |
assert ev.params == [a:'1',b:'2'] | |
} | |
/** */ | |
def isSectionCredit(evt){ | |
return evt.id == 'root' | |
} | |
/** | |
* Utility class for assert invocation counts. | |
* @see http://octodecillion.com/blog/behavior-counts-improve-junit/ | |
* @author jbetancourt | |
* | |
*/ | |
def class Counter { | |
private Map<String, Integer>counters = [DEFAULT__COUNTER:0] | |
String DEFAULT_COUNTER = "DEFAULT__COUNTER" | |
def get(){ | |
counters.get(DEFAULT_COUNTER) | |
} | |
def increment(){ | |
incrementValue(DEFAULT_COUNTER) | |
} | |
def assertCount(n){ | |
def v = get() | |
assert v == n | |
} | |
def increment(s){ | |
incrementValue(s) | |
} | |
def incrementValue(s){ | |
if (!counters.containsKey(s)) { | |
throw new IllegalStateException("counter not found: $s") | |
} | |
Integer value = counters.get(s) | |
counters.put(s, ++value) | |
value; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment