diff --git a/common/source/java/ch/systemsx/cisd/common/utilities/Template.java b/common/source/java/ch/systemsx/cisd/common/utilities/Template.java new file mode 100644 index 0000000000000000000000000000000000000000..6ea484ada7d5fa83df69e9090d0d37265b787dae --- /dev/null +++ b/common/source/java/ch/systemsx/cisd/common/utilities/Template.java @@ -0,0 +1,333 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.utilities; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * A little template engine. Usage example: + * <pre> + * Template template = new Template("Hello ${name}!"); + * template.bind("name", "world"); + * String text = template.createText(); + * </pre> + * The method {@link #bind(String, String)} throws an exception if the placeholder name is unknown. + * The method {@link #createText()} throws an exception if not all placeholders have been bound. + * <p> + * Since placeholder bindings change the state of an instance of this class there is method {@link #createFreshCopy()} + * which creates a copy without reparsing the template. Usage example: + * <pre> + * static final Template TEMPLATE = new Template("Hello ${name}!"); + * + * void doSomething() + * { + * Template template = TEMPLATE.createFreshCopy(); + * template.bind("name", "world"); + * String text = template.createText(); + * } + * </pre> + * + * @author Franz-Josef Elmer + */ +public class Template +{ + private static final char PLACEHOLDER_ESCAPE_CHARACTER = '$'; + private static final char PLACEHOLDER_START_CHARACTER = '{'; + private static final char PLACEHOLDER_END_CHARACTER = '}'; + + private static final String createPlaceholder(String variableName) + { + return PLACEHOLDER_ESCAPE_CHARACTER + (PLACEHOLDER_START_CHARACTER + variableName) + PLACEHOLDER_END_CHARACTER; + } + + private static interface IToken + { + public void appendTo(StringBuilder builder); + } + + private static final class PlainToken implements IToken + { + private final String plainText; + + PlainToken(String plainText) + { + assert plainText != null : "Unspecified plain text."; + this.plainText = plainText; + } + + public void appendTo(StringBuilder builder) + { + builder.append(plainText); + } + } + + private static final class VariableToken implements IToken + { + private final String variableName; + + private String value; + + VariableToken(String variablePlaceHolder) + { + assert variablePlaceHolder != null : "Unspecified variable place holder."; + this.variableName = variablePlaceHolder; + } + + public void appendTo(StringBuilder builder) + { + builder.append(isBound() ? value : createPlaceholder(variableName)); + } + + String getVariableName() + { + return variableName; + } + + boolean isBound() + { + return value != null; + } + + void bind(String v) + { + this.value = v; + } + } + + private static enum State + { + PLAIN() + { + @Override + State next(char character, TokenBuilder tokenBuilder) + { + if (character == PLACEHOLDER_ESCAPE_CHARACTER) + { + return STARTING_PLACEHOLDER; + } + tokenBuilder.addCharacter(character); + return PLAIN; + } + }, + + STARTING_PLACEHOLDER() + { + @Override + State next(char character, TokenBuilder tokenBuilder) + { + switch (character) + { + case PLACEHOLDER_ESCAPE_CHARACTER: + tokenBuilder.addCharacter(PLACEHOLDER_ESCAPE_CHARACTER); + return PLAIN; + case PLACEHOLDER_START_CHARACTER: + tokenBuilder.finishPlainToken(); + return PLACEHOLDER; + default: + tokenBuilder.addCharacter(PLACEHOLDER_ESCAPE_CHARACTER); + tokenBuilder.addCharacter(character); + return PLAIN; + } + } + }, + + PLACEHOLDER() + { + @Override + State next(char character, TokenBuilder tokenBuilder) + { + if (character == PLACEHOLDER_END_CHARACTER) + { + tokenBuilder.finishPlaceholder(); + return PLAIN; + } + tokenBuilder.addCharacter(character); + return PLACEHOLDER; + } + }; + + abstract State next(char character, TokenBuilder tokenBuilder); + } + + private static final class TokenBuilder + { + private final Map<String, VariableToken> variableTokens; + private final List<IToken> tokens; + private final StringBuilder builder; + + TokenBuilder(Map<String, VariableToken> variableTokens, List<IToken> tokens) + { + this.variableTokens = variableTokens; + this.tokens = tokens; + builder = new StringBuilder(); + } + + public void addCharacter(char character) + { + builder.append(character); + } + + public void finishPlainToken() + { + if (builder.length() > 0) + { + tokens.add(new PlainToken(builder.toString())); + builder.setLength(0); + } + } + + public void finishPlaceholder() + { + String variableName = builder.toString(); + if (variableName.length() == 0) + { + throw new IllegalArgumentException("Nameless placeholder " + createPlaceholder("") + " found."); + } + VariableToken token = variableTokens.get(variableName); + if (token == null) + { + token = new VariableToken(variableName); + variableTokens.put(variableName, token); + } + tokens.add(token); + builder.setLength(0); + } + } + + private final Map<String, VariableToken> variableTokens; + private final List<IToken> tokens; + + /** + * Creates a new instance for the specified template. + * + * @throws IllegalArgumentException if some error occurred during parsing. + */ + public Template(String template) + { + this(new LinkedHashMap<String, VariableToken>(), new ArrayList<IToken>()); + assert template != null : "Unspecified template."; + + TokenBuilder tokenBuilder = new TokenBuilder(variableTokens, tokens); + State state = State.PLAIN; + for (int i = 0, n = template.length(); i < n; i++) + { + state = state.next(template.charAt(i), tokenBuilder); + } + if (state != State.PLAIN) + { + throw new IllegalArgumentException("Incomplete placeholder detected at the end."); + } + tokenBuilder.finishPlainToken(); + } + + private Template(Map<String, VariableToken> variableTokens, List<IToken> tokens) + { + this.variableTokens = variableTokens; + this.tokens = tokens; + } + + /** + * Creates a copy of this template with no variable bindings. + */ + public Template createFreshCopy() + { + LinkedHashMap<String, VariableToken> map = new LinkedHashMap<String, VariableToken>(); + ArrayList<IToken> list = new ArrayList<IToken>(); + for (IToken token : tokens) + { + if (token instanceof VariableToken) + { + String variableName = ((VariableToken) token).getVariableName(); + VariableToken variableToken = new VariableToken(variableName); + map.put(variableName, variableToken); + list.add(variableToken); + } else + { + list.add(token); + } + } + return new Template(map, list); + } + + + /** + * Binds the specified value to the specified placeholder name. + * + * @throws IllegalArgumentException if placeholder is not known. + */ + public void bind(String placeholderName, String value) + { + assert placeholderName != null : "Unspecified placeholder name."; + assert value != null : "Unspecified value for '" + placeholderName + "'"; + + VariableToken variableToken = variableTokens.get(placeholderName); + if (variableToken == null) + { + throw new IllegalArgumentException("Unknown variable '" + placeholderName + "'."); + } + variableToken.bind(value); + } + + /** + * Creates the text by using all placeholder bindings. + * + * @throws IllegalStateException if not all placeholders have been bound. + */ + public String createText() + { + return createText(true); + } + + /** + * Creates the text by using placeholder bindings. + * + * @param complete If <code>true</code> an {@link IllegalStateException} will be thrown if not all + * bindings are set. + */ + public String createText(boolean complete) + { + if (complete) + { + assertAllVariablesAreBound(); + } + StringBuilder builder = new StringBuilder(); + for (IToken token : tokens) + { + token.appendTo(builder); + } + return builder.toString(); + } + + private void assertAllVariablesAreBound() + { + StringBuilder builder = new StringBuilder(); + for (Map.Entry<String, VariableToken> entry : variableTokens.entrySet()) + { + if (entry.getValue().isBound() == false) + { + builder.append(entry.getKey()).append(' '); + } + } + if (builder.length() > 0) + { + throw new IllegalStateException("The following variables are not bound: " + builder); + } + } +} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/utilities/TemplateTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/TemplateTest.java new file mode 100644 index 0000000000000000000000000000000000000000..abba46a01e37100df4c02c60e7e6c699f7128bf2 --- /dev/null +++ b/common/sourceTest/java/ch/systemsx/cisd/common/utilities/TemplateTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2008 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.common.utilities; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import org.testng.annotations.Test; + +/** + * + * + * @author Franz-Josef Elmer + */ +public class TemplateTest +{ + @Test + public void testEmptyTemplate() + { + assertEquals("", new Template("").createText()); + } + + @Test + public void testWithoutPlaceholders() + { + assertEquals("hello", new Template("hello").createText()); + } + + @Test + public void testWithOnePlaceholder() + { + Template template = new Template("hello ${name}!"); + template.bind("name", "world"); + assertEquals("hello world!", template.createText()); + } + + @Test + public void testWithTwiceTheSamePlaceholder() + { + Template template = new Template("hello ${name}${name}"); + template.bind("name", "world"); + assertEquals("hello worldworld", template.createText()); + } + + @Test + public void testWithTwoPlaceholders() + { + Template template = new Template("hello ${name}, do you know ${name2}?"); + template.bind("name", "world"); + template.bind("name2", "Albert Einstein"); + assertEquals("hello world, do you know Albert Einstein?", template.createText()); + } + + @Test + public void testWithEscaping() + { + Template template = new Template("hello $${name}. I have 25$."); + assertEquals("hello ${name}. I have 25$.", template.createText()); + } + + @Test + public void testNamelessPlaceholderInTemplate() + { + try + { + new Template("hello ${}"); + fail("IllegalArgumentException expected."); + } catch (IllegalArgumentException e) + { + assertEquals("Nameless placeholder ${} found.", e.getMessage()); + } + } + + @Test + public void testUnfinishedPlaceholderInTemplate() + { + try + { + new Template("hello ${name"); + fail("IllegalArgumentException expected."); + } catch (IllegalArgumentException e) + { + assertEquals("Incomplete placeholder detected at the end.", e.getMessage()); + } + try + { + new Template("hello ${"); + fail("IllegalArgumentException expected."); + } catch (IllegalArgumentException e) + { + assertEquals("Incomplete placeholder detected at the end.", e.getMessage()); + } + try + { + new Template("hello $"); + fail("IllegalArgumentException expected."); + } catch (IllegalArgumentException e) + { + assertEquals("Incomplete placeholder detected at the end.", e.getMessage()); + } + } + + @Test + public void testBindUnknownPlaceholder() + { + Template template = new Template("hello ${name}!"); + try + { + template.bind("blabla", "blub"); + fail("IllegalArgumentException expected."); + } catch (IllegalArgumentException e) + { + assertEquals("Unknown variable 'blabla'.", e.getMessage()); + } + } + + @Test + public void testIncompleteBinding() + { + Template template = new Template("${greeting} ${name}!"); + try + { + template.createText(true); + fail("IllegalStateException expected"); + } catch (IllegalStateException e) + { + assertEquals("The following variables are not bound: greeting name ", e.getMessage()); + } + + template.bind("greeting", "hello"); + assertEquals("hello ${name}!", template.createText(false)); + try + { + template.createText(true); + fail("IllegalStateException expected"); + } catch (IllegalStateException e) + { + assertEquals("The following variables are not bound: name ", e.getMessage()); + } + } + + @Test + public void testCreateFreshCopy() + { + Template template = new Template("hello ${name}!"); + Template template1 = template.createFreshCopy(); + template1.bind("name", "world"); + assertEquals("hello world!", template1.createText()); + assertEquals("hello ${name}!", template.createText(false)); + + Template template2 = template.createFreshCopy(); + template2.bind("name", "universe"); + assertEquals("hello universe!", template2.createText()); + assertEquals("hello world!", template1.createText()); + assertEquals("hello ${name}!", template.createText(false)); + } +}