Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
You can’t perform that action at this time.