Skip to content

Instantly share code, notes, and snippets.

@josefbetancourt
Last active December 29, 2015 17:09
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 josefbetancourt/7701645 to your computer and use it in GitHub Desktop.
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"
# 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]
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
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