Skip to content

Instantly share code, notes, and snippets.

@rjeschke
Last active December 3, 2015 13:12
Show Gist options
  • Save rjeschke/81d3ba635d70b80c3eaf to your computer and use it in GitHub Desktop.
Save rjeschke/81d3ba635d70b80c3eaf to your computer and use it in GitHub Desktop.
Simple and fast, single-file template system, designed for: create once, render often/concurrently
/*
* Copyright (C) 2015 René Jeschke <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rjeschke.toolbox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* <p>
* Placeholders: <code>${key[:default value]}</code>
* </p>
* <ul>
* <li><code>key</code> is case sensitive and gets surrounding whitespace
* removed.</li>
* <li><code>default value</code> can be anything, does not get changed except
* for delimiter escape sequences (see below).</li>
* </ul>
* <p>
* Delimiter escape sequences:
* </p>
* <ul>
* <li><code>$\{</code> gets replaced by <code>${</code> outside of placeholders
* and inside default values.</li>
* <li><code>\}</code> only gets replaced by <code>}</code> inside default
* values.</li>
* </ul>
*
* @author René Jeschke &lt;[email protected]&gt;
*/
public class Template
{
private final String[] parts;
private final String[] defaults;
private final int[] keyIndices;
private final HashMap<String, Integer> keyMap;
private final int length;
private Template(final String[] parts,
final String[] defaults,
final int[] keyIndices,
final HashMap<String, Integer> keyMap)
{
this.parts = parts;
this.defaults = defaults;
this.keyIndices = keyIndices;
this.keyMap = keyMap;
int len = 0;
for (int i = 0; i < parts.length; i++)
{
len += parts[i].length();
}
this.length = len;
}
public Filler filler()
{
return new Filler(this);
}
public static class Filler
{
private final Template template;
private final CharSequence[] values;
private Filler(final Template template)
{
this.template = template;
this.values = new CharSequence[template.keyMap.size()];
this.reset();
}
public Filler reset()
{
System.arraycopy(this.template.defaults, 0, this.values, 0, this.values.length);
return this;
}
public Filler set(final String key, final CharSequence value)
{
final Integer idx = this.template.keyMap.get(key);
if (idx != null)
{
this.values[idx.intValue()] = value;
}
return this;
}
public Filler set(final Map<String, ? extends CharSequence> map)
{
for (final Entry<String, ? extends CharSequence> e : map.entrySet())
{
this.set(e.getKey(), e.getValue());
}
return this;
}
public int calculateSize()
{
int size = this.template.length;
for (int i = 0; i < this.template.keyIndices.length; i++)
{
final int idx = this.template.keyIndices[i];
if (idx > -1)
{
size += this.values[idx].length();
}
}
return size;
}
public String process()
{
final StringBuilder sb = new StringBuilder();
this.process(sb);
return sb.toString();
}
public Filler process(final StringBuilder out)
{
out.ensureCapacity(this.calculateSize());
for (int i = 0; i < this.template.parts.length; i++)
{
out.append(this.template.parts[i]);
final int index = this.template.keyIndices[i];
if (index > -1)
{
out.append(this.values[index]);
}
}
return this;
}
public Filler apply(final Collection<Map<String, ? extends CharSequence>> listOfMaps, final StringBuilder out)
{
return this.apply(listOfMaps, false, out);
}
public Filler apply(final Collection<Map<String, ? extends CharSequence>> listOfMaps, final boolean reset,
final StringBuilder out)
{
for (final Map<String, ? extends CharSequence> map : listOfMaps)
{
if (reset)
{
this.reset();
}
this.set(map);
this.process(out);
}
return this;
}
@Override
public String toString()
{
return this.process();
}
}
public static Template create(final String input)
{
final ArrayList<String> parts = new ArrayList<String>();
final HashMap<String, Integer> keys = new HashMap<String, Integer>();
final IntArrayBuilder inserts = new IntArrayBuilder(32);
final ArrayList<String> defaults = new ArrayList<String>();
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++)
{
final char current = input.charAt(i);
final char next = i + 1 < input.length() ? input.charAt(i + 1) : '\0';
final char next2 = i + 2 < input.length() ? input.charAt(i + 2) : '\0';
if (current == '$' && next == '\\' && next2 == '{')
{
sb.append("${");
i += 2;
}
else if (current == '$' && next == '{')
{
int n = i + 2;
boolean inDefault = false;
final StringBuilder key = new StringBuilder();
final StringBuilder def = new StringBuilder();
while (n < input.length())
{
final char c0 = input.charAt(n);
final char c1 = n + 1 < input.length() ? input.charAt(n + 1) : '\0';
if (!inDefault)
{
if (c0 == '}')
{
break;
}
else if (c0 == ':')
{
inDefault = true;
}
else
{
key.append(c0);
}
}
else
{
if (c0 == '\\' && c1 == '}')
{
def.append('}');
n++;
}
else if (c0 == '}')
{
break;
}
else
{
def.append(c0);
}
}
n++;
}
i = n;
parts.add(sb.toString());
sb.setLength(0);
final String sKey = key.toString().trim();
final String sDef = def.toString();
if (!keys.containsKey(sKey))
{
keys.put(sKey, Integer.valueOf(keys.size()));
defaults.add("");
}
final int kIdx = keys.get(sKey).intValue();
inserts.add(kIdx);
if (sDef.length() > 0)
{
defaults.set(kIdx, sDef);
}
}
else
{
sb.append(current);
}
}
parts.add(sb.toString());
inserts.add(-1);
return new Template(
parts.toArray(new String[parts.size()]),
defaults.toArray(new String[defaults.size()]),
inserts.toArray(),
keys);
}
private final static class IntArrayBuilder
{
private int[] array;
private int size;
public IntArrayBuilder(final int initialSize)
{
this.array = new int[Math.max(0, initialSize)];
}
public void add(final int x)
{
if (this.size == this.array.length)
{
this.resize();
}
this.array[this.size++] = x;
}
public int[] toArray()
{
return Arrays.copyOf(this.array, this.size);
}
private static int guardedDouble(final int x)
{
final int y = Math.max(1, x) << 1;
return y <= 0 ? Integer.MAX_VALUE : y;
}
private void resize()
{
final int newSize = guardedDouble(this.array.length);
if (this.array.length != newSize)
{
this.array = Arrays.copyOf(this.array, newSize);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment