Skip to content

Instantly share code, notes, and snippets.

@jvmvik
Last active December 28, 2015 08:09
Show Gist options
  • Save jvmvik/7469465 to your computer and use it in GitHub Desktop.
Save jvmvik/7469465 to your computer and use it in GitHub Desktop.
Very basic template engine... I wrote this to replace the horrible Groovy Template Engine. This is not designed to solve a large scope of problem.
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
/**
* Simple template parser.
*
* This parser offer to:
* - Replace $variable by the value
* - Replace ${variable} by the value
* - Disable the parsing of commented line
* - Store line parsed in memory during the processing when inMemory = true
* - Avoid memory starvation by keeping 1000 lines in memory max
*
* @author jvmvik@gmail.com
*/
class SimpleTemplate
{
// Optimisation settings
// Enables to write the content of the output buffer in the file line by line.
boolean inMemory = true
// Ignore line starting by a comment.
// Comment supported: # or //
boolean ignoreComment = false
void parse(Path source, Path target, Map<String, String> mapping)
throws SyntaxException
{
def writer, reader
try
{
writer = Files.newBufferedWriter(target, Charset.defaultCharset(), StandardOpenOption.CREATE)
reader = Files.newBufferedReader(source, Charset.defaultCharset());
String s
int j = 0
while ((s = reader.readLine()) != null)
{
writer.println(parseLine(s, mapping, j++, source))
if (!inMemory || // Use RAM
(j >= 1000 && j % 1000 == 0)) // Avoid memory starvation
writer.flush()
}
writer.flush() // flush only at the end for faster I/O performance
}
catch (Exception e)
{
writer.close()
reader.close()
Files.delete(target)
e.printStackTrace()
throw e
}
finally
{
writer.flush()
writer.close()
reader.close()
}
}
/***
* Parse a single line, and replace variables.
*
* @param s string to process
* @param mapping key:value pairs that should be replaced
* @param lineNumber current line read in the file
* @param file path of the template file which is read
* @return line where key are replaced by their value
* @throws SyntaxException
*/
String parseLine(String s, Map<String, String> mapping, int lineNumber, Path file)
throws SyntaxException
{
int i = 0
int offset, k
if (!s.contains('$'))
return s
// Find matches
String key
while ((i = s.indexOf('$', i)) > -1)
{
i++
if (!(ignoreComment
&& (s ==~ /$\s+#.*/ || s ==~ /$\s+\/\/.*/)))
{
if (s[i + 1] ==~ /\s+/)
{
throw new SyntaxException("\$ must not be follow by a white space", lineNumber, i + 1, file)
}
if ('{'.equals(s[i]))
{
if ((k = s.indexOf('}')) > -1)
key = s.substring(i + 1, k).trim()
else
throw new SyntaxException("\${ must end with }", lineNumber, i + 1, file)
offset = 2
}
else
{
if ((k = s.indexOf(' ', i)) > -1)
key = s.substring(i, k).trim()
else
key = s.substring(i).trim()
offset = 0
}
if (key.contains(' '))
throw new SyntaxException("key must not contain blank", lineNumber, i, file)
//Get value
String v
try
{
v = mapping.get(key)
if (!v)
throw new SyntaxException("Empty value for key: $key", lineNumber, i, file)
}
catch (NullPointerException ex)
{
throw new SyntaxException("Key is not found: $key", lineNumber, i, file)
}
// Replace
s = s.substring(0, i - 1) + v + s.substring(i + offset + key.length(), s.length())
}
}
return s
}
}
import com.arm.pipd.scf.exception.SyntaxException
import org.junit.Before
import org.junit.Test
import java.nio.file.Paths
import static groovy.test.GroovyAssert.shouldFail
/**
* @author jvmvik@gmail.com
*/
class SimpleTemplateTest
{
SimpleTemplate tpl
@Before
void setUp()
{
tpl = new SimpleTemplate()
}
@Test
void parse()
{
def i = File.createTempFile('scf-input','tmp')
i.withPrintWriter
{ writer ->
writer.println('line1: $key1 $key2')
writer.println('## \'line2: ${key3} ')
}
def o = File.createTempFile('scf-output','tmp')
def m = [key1: "hello", key2: "word", key3: 'this is fun to do..']
tpl.parse(i.toPath(), o.toPath(), m)
assert "line1: $m.key1 $m.key2" == o.readLines()[0]
assert "## 'line2: ${m.key3} " == o.readLines()[1]
o.delete()
i.delete()
}
@Test
void parseLine()
{
def f = Paths.get('none.txt')
assert 'hello world' == tpl.parseLine('hello $key', [key:'world'], 0, f)
assert 'hello world' == tpl.parseLine('hello ${key}', [key:'world'], 0, f)
assert 'hello \n world' == tpl.parseLine('hello \n ${key}', [key:'world'], 0, f)
assert 'hello \n world' == tpl.parseLine('${key1} \n ${key2}', [key1:'hello',key2:'world'], 0, f)
assert 'hello \n world' == tpl.parseLine('$key1 \n $key2', [key1:'hello',key2:'world'], 0, f)
}
@Test
void parseLineFail()
{
def f = Paths.get('none.txt')
shouldFail(SyntaxException, {
tpl.parseLine('hello $ key', [key:'world'], 0, f)
})
shouldFail(SyntaxException, {
tpl.parseLine('hello ${key', [key:'world'], 0, f)
})
shouldFail(SyntaxException, {
tpl.parseLine('hello ${ key }', [key:'world'], 0, f)
})
shouldFail(SyntaxException, {
tpl.parseLine('hello ${ key', [key:'world'], 0, f)
})
}
@Test
void parseIgnoreCommentAndNoInMemory()
{
tpl.ignoreComment = true
tpl.inMemory = false
def i = File.createTempFile('scf-input','tmp')
i.withPrintWriter
{ writer ->
writer.println('line1: $key1 $key2')
writer.println(' ## ${key3} ')
writer.println(' // ${key3} ')
}
def o = File.createTempFile('scf-output','tmp')
def m = [key1: "hello", key2: "word", key3: 'this is fun to do..']
tpl.parse(i.toPath(), o.toPath(), m)
assert "line1: $m.key1 $m.key2" == o.readLines()[0]
assert " ## ${m.key3} " == o.readLines()[1]
assert " // ${m.key3} " == o.readLines()[2]
o.delete()
i.delete()
}
}
import java.nio.file.Path
/**
* Syntax Exception
*
* Purpose
* - Show clear error message if a configuration file cannot be parse by ConfigSlurper
*
* @author jvmvik@gmail.com
*/
class SyntaxException extends ScfException
{
public SyntaxException(String message)
{
super(message)
}
public SyntaxException(String message, int line, int colNumber, Path path)
{
super("(${line}, ${colNumber}) ${message} in ${path}")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment