http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/com/kenshoo/freemarker/util/DataModelParser.java
----------------------------------------------------------------------
diff --git a/src/main/java/com/kenshoo/freemarker/util/DataModelParser.java 
b/src/main/java/com/kenshoo/freemarker/util/DataModelParser.java
new file mode 100644
index 0000000..28cf6ff
--- /dev/null
+++ b/src/main/java/com/kenshoo/freemarker/util/DataModelParser.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.util;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+
+import org.springframework.util.StringUtils;
+import org.w3c.dom.Document;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import freemarker.ext.dom.NodeModel;
+import freemarker.template.utility.DateUtil;
+import freemarker.template.utility.DateUtil.CalendarFieldsToDateConverter;
+import freemarker.template.utility.DateUtil.DateParseException;
+import 
freemarker.template.utility.DateUtil.TrivialCalendarFieldsToDateConverter;
+
+/**
+ * Parses the text that the user enters into the data model input field.
+ */
+public final class DataModelParser {
+
+    private static final String KEYWORD_NEGATIVE_INFINITY = "-Infinity";
+
+    private static final String KEYWORD_POSITIVE_INFINITY = "+Infinity";
+
+    private static final String KEYWORD_INFINITY = "Infinity";
+
+    private static final String KEYWORD_TRUE = "true";
+
+    private static final String KEYWORD_FALSE = "false";
+
+    private static final String KEYWORD_NULL = "null";
+
+    private static final String KEYWORD_NAN = "NaN";
+
+    /** Matches a line starting like "someVariable=". */
+    private static final Pattern ASSIGNMENT_START = Pattern.compile(
+            "^\\s*"
+            + "(\\p{L}[\\p{L}\\p{N}\\.:\\-_$@]*)" // name
+            + "[ \t]*=\\s*",
+            Pattern.MULTILINE);
+
+    /** Matches a value that starts like a number, or probably meant to be 
number at least. */
+    private static final Pattern NUMBER_LIKE = 
Pattern.compile("[+-]?[\\.,]?[0-9].*", Pattern.DOTALL);
+    
+    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
+
+    private DataModelParser() {
+        // Not meant to be instantiated
+    }
+
+    public static Map<String, Object> parse(String src, TimeZone timeZone) 
throws DataModelParsingException {
+        if (!StringUtils.hasText(src)) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, Object> dataModel = new LinkedHashMap<>();
+
+        String lastName = null;
+        int lastAssignmentStartEnd = 0;
+        final Matcher assignmentStart = ASSIGNMENT_START.matcher(src);
+        findAssignments: while (true) {
+            boolean hasNextAssignment = 
assignmentStart.find(lastAssignmentStartEnd);
+
+            if (lastName != null) {
+                String value = src.substring(
+                        lastAssignmentStartEnd, hasNextAssignment ? 
assignmentStart.start() : src.length())
+                        .trim();
+                final Object parsedValue;
+                try {
+                    parsedValue = parseValue(value, timeZone);
+                } catch (DataModelParsingException e) {
+                    throw new DataModelParsingException(
+                            "Failed to parse the value of \"" + lastName + 
"\":\n" + e.getMessage(), e.getCause());
+                }
+                dataModel.put(lastName, parsedValue);
+            }
+
+            if (lastName == null && (!hasNextAssignment || 
assignmentStart.start() != 0)) {
+                throw new DataModelParsingException(
+                        "The data model specification must start with an 
assignment (name=value).");
+            }
+
+            if (!hasNextAssignment) {
+                break findAssignments;
+            }
+
+            lastName = assignmentStart.group(1).trim();
+            lastAssignmentStartEnd = assignmentStart.end();
+        }
+
+        return dataModel;
+    }
+    
+    private static Object parseValue(String value, TimeZone timeZone) throws 
DataModelParsingException {
+        // Note: Because we fall back to interpret the input as a literal 
string value when it doesn't look like
+        // anything else (like a number, boolean, etc.), it's important to 
avoid misunderstandings, and throw exception
+        // in suspicious situations. The user can always quote the string 
value if we are "too smart". But he will
+        // be confused about the rules of FreeMarker if what he believes to be 
a non-string is misinterpreted by this
+        // parser as a string. Getting sometimes an error and then quoting the 
string is better than that.
+        
+        if (value.endsWith(";")) {  // Tolerate this habit of Java and 
JavaScript programmers
+            value = value.substring(value.length() - 1).trim();
+        }
+        
+        if (NUMBER_LIKE.matcher(value).matches()) {
+            try {
+                return new BigDecimal(value);
+            } catch (NumberFormatException e) {
+                // Maybe it's a ISO 8601 Date/time/datetime
+                CalendarFieldsToDateConverter calToDateConverter = new 
TrivialCalendarFieldsToDateConverter();
+                
+                DateParseException attemptedTemportalPExc = null;
+                String attemptedTemporalType = null;
+                final int dashIdx = value.indexOf('-');
+                final int colonIdx = value.indexOf(':');
+                if (value.indexOf('T') > 1 || (dashIdx > 1 && colonIdx > 
dashIdx)) {
+                    try {
+                        return new Timestamp(
+                                DateUtil.parseISO8601DateTime(value, timeZone, 
calToDateConverter).getTime());
+                    } catch (DateParseException pExc) {
+                        attemptedTemporalType = "date-time";
+                        attemptedTemportalPExc = pExc;
+                    }
+                } else if (dashIdx > 1) {
+                    try {
+                        return new java.sql.Date(
+                                DateUtil.parseISO8601Date(value, timeZone, 
calToDateConverter).getTime());
+                    } catch (DateParseException pExc) {
+                        attemptedTemporalType = "date";
+                        attemptedTemportalPExc = pExc;
+                    }
+                } else if (colonIdx > 1) { 
+                    try {
+                        return new Time(
+                                DateUtil.parseISO8601Time(value, timeZone, 
calToDateConverter).getTime());
+                    } catch (DateParseException pExc) {
+                        attemptedTemporalType = "time";
+                        attemptedTemportalPExc = pExc;
+                    }
+                }
+                if (attemptedTemportalPExc == null) {
+                    throw new DataModelParsingException("Malformed number: " + 
value, e);
+                } else {
+                    throw new DataModelParsingException(
+                            "Malformed ISO 8601 " + attemptedTemporalType + " 
(or malformed number): " + 
+                            attemptedTemportalPExc.getMessage(), e.getCause());
+                }
+            }
+        } else if (value.startsWith("\"")) {
+            try {
+                return JSON_MAPPER.readValue(value, String.class);
+            } catch (IOException e) {
+                throw new DataModelParsingException(
+                        "Malformed quoted string (using JSON syntax): " + 
getMessageWithoutLocation(e),
+                        e);
+            }
+        } else if (value.startsWith("\'")) {
+            throw new DataModelParsingException(
+                    "Malformed quoted string (using JSON syntax): Use \" 
character for quotation, not \' character.");
+        } else if (value.startsWith("[")) {
+            try {
+                return JSON_MAPPER.readValue(value, List.class);
+            } catch (IOException e) {
+                throw new DataModelParsingException(
+                        "Malformed list (using JSON syntax): " + 
getMessageWithoutLocation(e),
+                        e);
+            }
+        } else if (value.startsWith("{")) {
+            try {
+                return JSON_MAPPER.readValue(value, LinkedHashMap.class);
+            } catch (IOException e) {
+                throw new DataModelParsingException(
+                        "Malformed list (using JSON syntax): " + 
getMessageWithoutLocation(e),
+                        e);
+            }
+        } else if (value.startsWith("<")) {
+            try {
+                DocumentBuilder builder = 
NodeModel.getDocumentBuilderFactory().newDocumentBuilder();
+                ErrorHandler errorHandler = NodeModel.getErrorHandler();
+                if (errorHandler != null) 
builder.setErrorHandler(errorHandler);
+                final Document doc = builder.parse(new InputSource(new 
StringReader(value)));
+                NodeModel.simplify(doc);
+                return doc;
+            } catch (SAXException e) {
+                final String saxMsg = e.getMessage();
+                throw new DataModelParsingException("Malformed XML: " + 
(saxMsg != null ? saxMsg : e), e);
+            } catch (Exception e) {
+                throw new DataModelParsingException("XML parsing has failed 
with internal error: " + e, e);
+            }
+        } else if (value.equalsIgnoreCase(KEYWORD_TRUE)) {
+            checkKeywordCase(value, KEYWORD_TRUE);
+            return Boolean.TRUE;
+        } else if (value.equalsIgnoreCase(KEYWORD_FALSE)) {
+            checkKeywordCase(value, KEYWORD_FALSE);
+            return Boolean.FALSE;
+        } else if (value.equalsIgnoreCase(KEYWORD_NULL)) {
+            checkKeywordCase(value, KEYWORD_NULL);
+            return null;
+        } else if (value.equalsIgnoreCase(KEYWORD_NAN)) {
+            checkKeywordCase(value, KEYWORD_NAN);
+            return Double.NaN;
+        } else if (value.equalsIgnoreCase(KEYWORD_INFINITY)) {
+            checkKeywordCase(value, KEYWORD_INFINITY);
+            return Double.POSITIVE_INFINITY;
+        } else if (value.equalsIgnoreCase(KEYWORD_POSITIVE_INFINITY)) {
+            checkKeywordCase(value, KEYWORD_POSITIVE_INFINITY);
+            return Double.POSITIVE_INFINITY;
+        } else if (value.equalsIgnoreCase(KEYWORD_NEGATIVE_INFINITY)) {
+            checkKeywordCase(value, KEYWORD_NEGATIVE_INFINITY);
+            return Double.NEGATIVE_INFINITY;
+        } else if (value.length() == 0) {
+            throw new DataModelParsingException(
+                    "Empty value. (If you indeed wanted a 0 length string, 
quote it, like \"\".)");
+        } else {
+            return value;
+        }
+    }
+
+    private static String getMessageWithoutLocation(IOException e) {
+        return e instanceof JsonProcessingException
+                ? ((JsonProcessingException) e).getOriginalMessage()
+                : e.getMessage();
+    }
+
+    private static void checkKeywordCase(String inputKeyword, String 
correctKeyword) throws DataModelParsingException {
+        if (!correctKeyword.equals(inputKeyword)) {
+            throw new DataModelParsingException("Keywords are case sensitive; 
the correct form is: "
+                    + correctKeyword);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/com/kenshoo/freemarker/util/DataModelParsingException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/com/kenshoo/freemarker/util/DataModelParsingException.java 
b/src/main/java/com/kenshoo/freemarker/util/DataModelParsingException.java
new file mode 100644
index 0000000..cd24407
--- /dev/null
+++ b/src/main/java/com/kenshoo/freemarker/util/DataModelParsingException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.util;
+
+import java.util.TimeZone;
+
+/**
+ * Thrown by {@link DataModelParser#parse(String, TimeZone)}.
+ */
+public class DataModelParsingException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public DataModelParsingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DataModelParsingException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/com/kenshoo/freemarker/util/ExceptionUtils.java
----------------------------------------------------------------------
diff --git a/src/main/java/com/kenshoo/freemarker/util/ExceptionUtils.java 
b/src/main/java/com/kenshoo/freemarker/util/ExceptionUtils.java
new file mode 100644
index 0000000..999c450
--- /dev/null
+++ b/src/main/java/com/kenshoo/freemarker/util/ExceptionUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.util;
+
+import freemarker.core.ParseException;
+import freemarker.template.TemplateException;
+
+public final class ExceptionUtils {
+
+    private ExceptionUtils() {
+        // Not meant to be instantiated
+    }
+
+    /**
+     * The error message (and sometimes also the class), and then the same 
with the cause exception, and so on. Doesn't
+     * contain the stack trace or other location information.
+     */
+    public static String getMessageWithCauses(final Throwable exc) {
+        StringBuilder sb = new StringBuilder();
+        
+        Throwable curExc = exc;
+        while (curExc != null) {
+            if (curExc != exc) {
+                sb.append("\n\nCaused by:\n");
+            }
+            String msg = curExc.getMessage();
+            if (msg == null || !(curExc instanceof TemplateException || curExc 
instanceof ParseException)) {
+                sb.append(curExc.getClass().getName()).append(": ");
+            }
+            sb.append(msg);
+            curExc = curExc.getCause();
+        }
+        return sb.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/com/kenshoo/freemarker/util/LengthLimitExceededException.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/com/kenshoo/freemarker/util/LengthLimitExceededException.java 
b/src/main/java/com/kenshoo/freemarker/util/LengthLimitExceededException.java
new file mode 100644
index 0000000..67effd0
--- /dev/null
+++ 
b/src/main/java/com/kenshoo/freemarker/util/LengthLimitExceededException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.util;
+
+import java.io.IOException;
+
+/**
+ * Thrown by {@link LengthLimitedWriter}.
+ */
+public class LengthLimitExceededException extends IOException {
+
+    private static final long serialVersionUID = 1L;
+    
+    public LengthLimitExceededException() {
+        super("The outout String length limit of the Writer was exceeded.");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/com/kenshoo/freemarker/util/LengthLimitedWriter.java
----------------------------------------------------------------------
diff --git a/src/main/java/com/kenshoo/freemarker/util/LengthLimitedWriter.java 
b/src/main/java/com/kenshoo/freemarker/util/LengthLimitedWriter.java
new file mode 100644
index 0000000..a4d1450
--- /dev/null
+++ b/src/main/java/com/kenshoo/freemarker/util/LengthLimitedWriter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.util;
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+
+/**
+ * A {@link StringWriter} that limits its buffer size, and throws {@link 
LengthLimitExceededException} when that's
+ * exceeded.
+ */
+public class LengthLimitedWriter extends FilterWriter {
+    
+    private int lengthLeft;
+
+    public LengthLimitedWriter(Writer writer, int lengthLimit) {
+        super(writer);
+        this.lengthLeft = lengthLimit;
+    }
+
+    @Override
+    public void write(int c) throws IOException {
+        if (lengthLeft < 1) {
+            throw new LengthLimitExceededException();
+        }
+        
+        super.write(c);
+        
+        lengthLeft--;
+    }
+
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        final boolean lengthExceeded;
+        if (lengthLeft < len) {
+            len = lengthLeft;
+            lengthExceeded = true;
+        } else {
+            lengthExceeded = false;
+        }
+        
+        super.write(cbuf, off, len);
+        lengthLeft -= len;
+        
+        if (lengthExceeded) {
+            throw new LengthLimitExceededException();
+        }
+    }
+
+    @Override
+    public void write(String str, int off, int len) throws IOException {
+        final boolean lengthExceeded;
+        if (lengthLeft < len) {
+            len = lengthLeft;
+            lengthExceeded = true;
+        } else {
+            lengthExceeded = false;
+        }
+        
+        super.write(str, off, len);
+        lengthLeft -= len;
+        
+        if (lengthExceeded) {
+            throw new LengthLimitExceededException();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/com/kenshoo/freemarker/view/FreeMarkerOnlineView.java
----------------------------------------------------------------------
diff --git 
a/src/main/java/com/kenshoo/freemarker/view/FreeMarkerOnlineView.java 
b/src/main/java/com/kenshoo/freemarker/view/FreeMarkerOnlineView.java
new file mode 100644
index 0000000..684511f
--- /dev/null
+++ b/src/main/java/com/kenshoo/freemarker/view/FreeMarkerOnlineView.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.view;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.kenshoo.freemarker.model.SelectionOption;
+import com.kenshoo.freemarker.services.AllowedSettingValuesMaps;
+import com.yammer.dropwizard.views.View;
+
+import freemarker.template.Configuration;
+
+/**
+ * Created with IntelliJ IDEA. User: nir Date: 4/11/14 Time: 12:23 PM
+ */
+public class FreeMarkerOnlineView extends View {
+
+    private static final List<SelectionOption> LOCALE_SELECTION_OPTIONS = 
toLocaleSelectionOptions(AllowedSettingValuesMaps.LOCALE_MAP);
+    private static final List<SelectionOption> TIME_ZONE_SELECTION_OPTIONS = 
toSelectionOptions(AllowedSettingValuesMaps.TIME_ZONE_MAP);
+    private static final List<SelectionOption> OUTPUT_FORMAT_SELECTION_OPTIONS 
= toSelectionOptions(AllowedSettingValuesMaps.OUTPUT_FORMAT_MAP);
+    
+    private String template = "";
+    private String dataModel = "";
+    private String outputFormat = 
AllowedSettingValuesMaps.DEFAULT_OUTPUT_FORMAT_KEY;
+    private String locale = AllowedSettingValuesMaps.DEFAULT_LOCALE_KEY;
+    private String timeZone = AllowedSettingValuesMaps.DEFAULT_TIME_ZONE_KEY;
+    
+    private boolean execute;
+    
+    private static List<SelectionOption> toSelectionOptions(Map<String, ?> 
settingValueMap) {
+        ArrayList<SelectionOption> selectionOptions = new 
ArrayList<SelectionOption>(settingValueMap.size());
+        for (String key : settingValueMap.keySet()) {
+            selectionOptions.add(new SelectionOption(key, truncate(key, 25)));
+        }
+        Collections.sort(selectionOptions);
+        return selectionOptions;
+    }
+    
+    private static List<SelectionOption> toLocaleSelectionOptions(Map<String, 
Locale> localeMap) {
+        ArrayList<SelectionOption> selectionOptions = new 
ArrayList<SelectionOption>(localeMap.size());
+        for (Map.Entry<String, Locale> ent : localeMap.entrySet()) {
+            Locale locale = ent.getValue();
+            selectionOptions.add(
+                    new SelectionOption(ent.getKey(),
+                    truncate(locale.getDisplayName(Locale.US), 18) + "; " + 
locale.toString()));
+        }
+        Collections.sort(selectionOptions);
+        return selectionOptions;
+    }
+    
+    private static String truncate(String s, int maxLength) {
+        if (s == null) {
+            return null;
+        }
+        return s.length() <= maxLength ? s : s.substring(0, Math.max(maxLength 
- 3, 0)) + "[...]";
+    }    
+
+    /**
+     *
+     * @param template
+     * @param dataModel
+     * @param execute set to true if the execution should be triggered on page 
load.
+     */
+    public FreeMarkerOnlineView() {
+        super("/view/freemarker-online.ftl", Charset.forName("utf-8"));
+    }
+
+    public String getTemplate() {
+        return template;
+    }
+
+    public void setTemplate(String template) {
+        this.template = withDefault(template, "");
+    }
+
+    public String getDataModel() {
+        return dataModel;
+    }
+
+    public void setDataModel(String dataModel) {
+        this.dataModel = withDefault(dataModel, "");
+    }
+
+    public String getFreeMarkerVersion() {
+        return Configuration.getVersion().toString();
+    }
+    
+    public List<SelectionOption> getOutputFormats() {
+        return OUTPUT_FORMAT_SELECTION_OPTIONS;
+    }
+
+    public List<SelectionOption> getLocales() {
+        return LOCALE_SELECTION_OPTIONS;
+    }
+
+    public List<SelectionOption> getTimeZones() {
+        return TIME_ZONE_SELECTION_OPTIONS;
+    }
+
+    public String getOutputFormat() {
+        return outputFormat;
+    }
+
+    public void setOutputFormat(String outputFormat) {
+        this.outputFormat = withDefault(outputFormat, 
AllowedSettingValuesMaps.DEFAULT_OUTPUT_FORMAT_KEY);
+    }
+
+    public String getLocale() {
+        return locale;
+    }
+
+    public void setLocale(String locale) {
+        this.locale = withDefault(locale, 
AllowedSettingValuesMaps.DEFAULT_LOCALE_KEY);
+    }
+
+    public String getTimeZone() {
+        return timeZone;
+    }
+
+    public void setTimeZone(String timeZone) {
+        this.timeZone = withDefault(timeZone, 
AllowedSettingValuesMaps.DEFAULT_TIME_ZONE_KEY);
+    }
+    
+    public boolean isExecute() {
+        return execute;
+    }
+
+    public void setExecute(boolean executeImmediately) {
+        this.execute = executeImmediately;
+    }
+
+    private static String withDefault(String value, String defaultValue) {
+        return !StringUtils.isBlank(value) ? value : defaultValue;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/java/freemarker/core/FreeMarkerInternalsAccessor.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/FreeMarkerInternalsAccessor.java 
b/src/main/java/freemarker/core/FreeMarkerInternalsAccessor.java
new file mode 100644
index 0000000..1ffc7f7
--- /dev/null
+++ b/src/main/java/freemarker/core/FreeMarkerInternalsAccessor.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 freemarker.core;
+
+import 
freemarker.core.ThreadInterruptionSupportTemplatePostProcessor.TemplateProcessingThreadInterruptedException;
+import freemarker.template.Template;
+
+/**
+ * Functions that depend on unpublished FreeMarker functionality. Might need 
to be adjusted for new FreeMarker releases.
+ * The relevant parts of the FreeMarker source code contains comments about 
keeping this in sync with that, so,
+ * hopefully this won't be a problem.
+ */
+public final class FreeMarkerInternalsAccessor {
+
+    /**
+     * Ensures that the template will react to {@link 
#interruptTemplateProcessing(Thread)}. 
+     */
+    public static void makeTemplateInterruptable(Template template) {
+        _CoreAPI.addThreadInterruptedChecks(template);
+    }
+
+    /**
+     * Checks if the template processing has thrown exception because of a 
{@link #interruptTemplateProcessing(Thread)}
+     * call.
+     */
+    public static boolean isTemplateProcessingInterruptedException(Throwable 
e) {
+        return e instanceof TemplateProcessingThreadInterruptedException;
+    }
+
+    /**
+     * Tells a template processing in another thread to abort; asynchronous.
+     */
+    public static void interruptTemplateProcessing(Thread t) {
+        t.interrupt();
+    }
+
+    /**
+     * Called from the thread where the interruptible template execution ran 
earlier, to clear any related thread state.
+     */
+    public static void clearAnyPendingTemplateProcessingInterruption() {
+        Thread.interrupted();  // To clears the interruption flag 
+    }
+    
+    private FreeMarkerInternalsAccessor() {
+        // Not meant to be instantiated
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/assets/css/main.css
----------------------------------------------------------------------
diff --git a/src/main/resources/assets/css/main.css 
b/src/main/resources/assets/css/main.css
new file mode 100644
index 0000000..44b8972
--- /dev/null
+++ b/src/main/resources/assets/css/main.css
@@ -0,0 +1,114 @@
+.clear {
+  clear: both;
+}
+
+#layout {
+       position: relative;
+       padding: 0;
+}
+
+.header {
+       margin: 0;
+       padding: 1em 0;
+       border-bottom: 1px solid #eee;
+       text-align: center;
+       color: #444;
+}
+
+.content {
+    margin: 0 auto;
+    padding: 1em 0;
+    width: 920px;
+}
+
+.footer {
+    margin: 0;
+    padding: 1em 2em 0;
+    border-top: 1px solid #eee;
+    text-align: center;
+    color: #444;
+}
+
+.errorMessage {
+    font-size: small;
+    color: red;
+    display: none;
+}
+
+.header h1 {
+    margin: 0.2em 0;
+    font-size: 1.75em;
+    font-weight: normal;
+}
+
+#result {
+    background-color: #FFF;
+    color: #000        
+}
+
+#result.error {
+    background-color: #FFF0F0;
+       color: #A00;
+}
+
+.faint {
+       color: #bbb
+}
+
+textarea.source-code {
+       font-family: monospace !important;
+    /* We set a few more things to decrease the chance of jQuery Autosize 
plugin issues: */
+    font-size: 1em !important;  
+       resize: none !important;
+    line-height: 1em !important;
+}
+
+.hiddenByDefault {
+   display: none;
+}
+
+#dataModelExamples {
+       background: #eee;
+       padding: 4px;
+       margin: 0;
+}
+
+#dataModelExamples .description {
+       font-style: italic;
+       margin-bottom: 1em;
+}
+
+#dataModelExamples pre {
+  font-family: monospace;
+  font-size: 1em;
+  padding: 0;
+  margin: 0;
+}
+
+#templateAndModelForm input,
+#templateAndModelForm textarea,
+#templateAndModelForm button {
+       margin-bottom: 1em;
+}
+
+#templateAndModelForm label {
+    margin-top: 1em;
+    margin-bottom: 0.25em;
+}
+
+.formPanel {
+       
+}
+
+.formBottomButtonsContainer {
+    margin-top: 1.5em;
+}
+
+.resultContainer {
+    margin-top: 1em;
+}
+
+.horizontalBox {
+    display: inline-block;
+               padding-right: 1em;     
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/assets/js/autosize.min.js
----------------------------------------------------------------------
diff --git a/src/main/resources/assets/js/autosize.min.js 
b/src/main/resources/assets/js/autosize.min.js
new file mode 100644
index 0000000..98138c9
--- /dev/null
+++ b/src/main/resources/assets/js/autosize.min.js
@@ -0,0 +1,6 @@
+/*!
+       Autosize 3.0.8
+       license: MIT
+       http://www.jacklmoore.com/autosize
+*/
+!function(e,t){if("function"==typeof 
define&&define.amd)define(["exports","module"],t);else if("undefined"!=typeof 
exports&&"undefined"!=typeof module)t(exports,module);else{var 
o={exports:{}};t(o.exports,o),e.autosize=o.exports}}(this,function(e,t){"use 
strict";function o(e){function t(){var 
t=window.getComputedStyle(e,null);"vertical"===t.resize?e.style.resize="none":"both"===t.resize&&(e.style.resize="horizontal"),u="content-box"===t.boxSizing?-(parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)):parseFloat(t.borderTopWidth)+parseFloat(t.borderBottomWidth),i()}function
 o(t){var 
o=e.style.width;e.style.width="0px",e.offsetWidth,e.style.width=o,v=t,l&&(e.style.overflowY=t),n()}function
 n(){var 
t=window.pageYOffset,o=document.body.scrollTop,n=e.style.height;e.style.height="auto";var
 i=e.scrollHeight+u;return 
0===e.scrollHeight?void(e.style.height=n):(e.style.height=i+"px",document.documentElement.scrollTop=t,void(document.body.scrollTop=o))}function
 i(){var t=e.style.height;n();va
 r 
i=window.getComputedStyle(e,null);if(i.height!==e.style.height?"visible"!==v&&o("visible"):"hidden"!==v&&o("hidden"),t!==e.style.height){var
 
r=document.createEvent("Event");r.initEvent("autosize:resized",!0,!1),e.dispatchEvent(r)}}var
 r=void 0===arguments[1]?{}:arguments[1],d=r.setOverflowX,s=void 
0===d?!0:d,a=r.setOverflowY,l=void 
0===a?!0:a;if(e&&e.nodeName&&"TEXTAREA"===e.nodeName&&!e.hasAttribute("data-autosize-on")){var
 
u=null,v="hidden",f=function(t){window.removeEventListener("resize",i),e.removeEventListener("input",i),e.removeEventListener("keyup",i),e.removeAttribute("data-autosize-on"),e.removeEventListener("autosize:destroy",f),Object.keys(t).forEach(function(o){e.style[o]=t[o]})}.bind(e,{height:e.style.height,resize:e.style.resize,overflowY:e.style.overflowY,overflowX:e.style.overflowX,wordWrap:e.style.wordWrap});e.addEventListener("autosize:destroy",f),"onpropertychange"in
 e&&"oninput"in 
e&&e.addEventListener("keyup",i),window.addEventListener("resize",i),e.addEventL
 
istener("input",i),e.addEventListener("autosize:update",i),e.setAttribute("data-autosize-on",!0),l&&(e.style.overflowY="hidden"),s&&(e.style.overflowX="hidden",e.style.wordWrap="break-word"),t()}}function
 n(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName){var 
t=document.createEvent("Event");t.initEvent("autosize:destroy",!0,!1),e.dispatchEvent(t)}}function
 i(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName){var 
t=document.createEvent("Event");t.initEvent("autosize:update",!0,!1),e.dispatchEvent(t)}}var
 r=null;"undefined"==typeof window||"function"!=typeof 
window.getComputedStyle?(r=function(e){return e},r.destroy=function(e){return 
e},r.update=function(e){return e}):(r=function(e,t){return 
e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return 
o(e,t)}),e},r.destroy=function(e){return 
e&&Array.prototype.forEach.call(e.length?e:[e],n),e},r.update=function(e){return
 e&&Array.prototype.forEach.call(e.length?e:[e],i),e}),t.exports=r});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/assets/js/jquery.autosize.min.js
----------------------------------------------------------------------
diff --git a/src/main/resources/assets/js/jquery.autosize.min.js 
b/src/main/resources/assets/js/jquery.autosize.min.js
new file mode 100644
index 0000000..5c764dc
--- /dev/null
+++ b/src/main/resources/assets/js/jquery.autosize.min.js
@@ -0,0 +1,6 @@
+/*!
+       Autosize 1.18.17
+       license: MIT
+       http://www.jacklmoore.com/autosize
+*/
+!function(e){var 
t,o={className:"autosizejs",id:"autosizejs",append:"\n",callback:!1,resizeDelay:10,placeholder:!0},i='<textarea
 tabindex="-1" style="position:absolute; top:-999px; left:0; right:auto; 
bottom:auto; border:0; padding: 0; -moz-box-sizing:content-box; 
-webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; 
height:0 !important; min-height:0 !important; overflow:hidden; transition:none; 
-webkit-transition:none; 
-moz-transition:none;"/>',a=["fontFamily","fontSize","fontWeight","fontStyle","letterSpacing","textTransform","wordSpacing","textIndent","whiteSpace"],n=e(i).data("autosize",!0)[0];n.style.lineHeight="99px","99px"===e(n).css("lineHeight")&&a.push("lineHeight"),n.style.lineHeight="",e.fn.autosize=function(i){return
 
this.length?(i=e.extend({},o,i||{}),n.parentNode!==document.body&&e(document.body).append(n),this.each(function(){function
 o(){var 
t,o=window.getComputedStyle?window.getComputedStyle(u,null):null;o?(t=parseFloat(o.width),("border-box
 
"===o.boxSizing||"border-box"===o.webkitBoxSizing||"border-box"===o.mozBoxSizing)&&e.each(["paddingLeft","paddingRight","borderLeftWidth","borderRightWidth"],function(e,i){t-=parseFloat(o[i])})):t=p.width(),n.style.width=Math.max(t,0)+"px"}function
 s(){var 
s={};if(t=u,n.className=i.className,n.id=i.id,d=parseFloat(p.css("maxHeight")),e.each(a,function(e,t){s[t]=p.css(t)}),e(n).css(s).attr("wrap",p.attr("wrap")),o(),window.chrome){var
 r=u.style.width;u.style.width="0px";{u.offsetWidth}u.style.width=r}}function 
r(){var 
e,a;t!==u?s():o(),n.value=!u.value&&i.placeholder?p.attr("placeholder")||"":u.value,n.value+=i.append||"",n.style.overflowY=u.style.overflowY,a=parseFloat(u.style.height)||0,n.scrollTop=0,n.scrollTop=9e4,e=n.scrollTop,d&&e>d?(u.style.overflowY="scroll",e=d):(u.style.overflowY="hidden",c>e&&(e=c)),e+=z,Math.abs(a-e)>.01&&(u.style.height=e+"px",n.className=n.className,w&&i.callback.call(u,u),p.trigger("autosize.resized"))}function
 l(){clearTimeout(h),h=setTimeout(function
 (){var e=p.width();e!==b&&(b=e,r())},parseInt(i.resizeDelay,10))}var 
d,c,h,u=this,p=e(u),z=0,w=e.isFunction(i.callback),f={height:u.style.height,overflow:u.style.overflow,overflowY:u.style.overflowY,wordWrap:u.style.wordWrap,resize:u.style.resize},b=p.width(),g=p.css("resize");p.data("autosize")||(p.data("autosize",!0),("border-box"===p.css("box-sizing")||"border-box"===p.css("-moz-box-sizing")||"border-box"===p.css("-webkit-box-sizing"))&&(z=p.outerHeight()-p.height()),c=Math.max(parseFloat(p.css("minHeight"))-z||0,p.height()),p.css({overflow:"hidden",overflowY:"hidden",wordWrap:"break-word"}),"vertical"===g?p.css("resize","none"):"both"===g&&p.css("resize","horizontal"),"onpropertychange"in
 u?"oninput"in u?p.on("input.autosize 
keyup.autosize",r):p.on("propertychange.autosize",function(){"value"===event.propertyName&&r()}):p.on("input.autosize",r),i.resizeDelay!==!1&&e(window).on("resize.autosize",l),p.on("autosize.resize",r),p.on("autosize.resizeIncludeStyle",function(){t=null,r()
 
}),p.on("autosize.destroy",function(){t=null,clearTimeout(h),e(window).off("resize",l),p.off("autosize").off(".autosize").css(f).removeData("autosize")}),r())})):this}}(jQuery||$);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/assets/js/jquery.blockUI.js
----------------------------------------------------------------------
diff --git a/src/main/resources/assets/js/jquery.blockUI.js 
b/src/main/resources/assets/js/jquery.blockUI.js
new file mode 100644
index 0000000..90ce5d6
--- /dev/null
+++ b/src/main/resources/assets/js/jquery.blockUI.js
@@ -0,0 +1,620 @@
+/*!
+ * jQuery blockUI plugin
+ * Version 2.70.0-2014.11.23
+ * Requires jQuery v1.7 or later
+ *
+ * Examples at: http://malsup.com/jquery/block/
+ * Copyright (c) 2007-2013 M. Alsup
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Thanks to Amir-Hossein Sobhi for some excellent contributions!
+ */
+
+;(function() {
+/*jshint eqeqeq:false curly:false latedef:false */
+"use strict";
+
+       function setup($) {
+               $.fn._fadeIn = $.fn.fadeIn;
+
+               var noOp = $.noop || function() {};
+
+               // this bit is to ensure we don't call setExpression when we 
shouldn't (with extra muscle to handle
+               // confusing userAgent strings on Vista)
+               var msie = /MSIE/.test(navigator.userAgent);
+               var ie6  = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 
8.0/.test(navigator.userAgent);
+               var mode = document.documentMode || 0;
+               var setExpr = $.isFunction( 
document.createElement('div').style.setExpression );
+
+               // global $ methods for blocking/unblocking the entire page
+               $.blockUI   = function(opts) { install(window, opts); };
+               $.unblockUI = function(opts) { remove(window, opts); };
+
+               // convenience method for quick growl-like notifications  
(http://www.google.com/search?q=growl)
+               $.growlUI = function(title, message, timeout, onClose) {
+                       var $m = $('<div class="growlUI"></div>');
+                       if (title) $m.append('<h1>'+title+'</h1>');
+                       if (message) $m.append('<h2>'+message+'</h2>');
+                       if (timeout === undefined) timeout = 3000;
+
+                       // Added by konapun: Set timeout to 30 seconds if this 
growl is moused over, like normal toast notifications
+                       var callBlock = function(opts) {
+                               opts = opts || {};
+
+                               $.blockUI({
+                                       message: $m,
+                                       fadeIn : typeof opts.fadeIn  !== 
'undefined' ? opts.fadeIn  : 700,
+                                       fadeOut: typeof opts.fadeOut !== 
'undefined' ? opts.fadeOut : 1000,
+                                       timeout: typeof opts.timeout !== 
'undefined' ? opts.timeout : timeout,
+                                       centerY: false,
+                                       showOverlay: false,
+                                       onUnblock: onClose,
+                                       css: $.blockUI.defaults.growlCSS
+                               });
+                       };
+
+                       callBlock();
+                       var nonmousedOpacity = $m.css('opacity');
+                       $m.mouseover(function() {
+                               callBlock({
+                                       fadeIn: 0,
+                                       timeout: 30000
+                               });
+
+                               var displayBlock = $('.blockMsg');
+                               displayBlock.stop(); // cancel fadeout if it 
has started
+                               displayBlock.fadeTo(300, 1); // make it easier 
to read the message by removing transparency
+                       }).mouseout(function() {
+                               $('.blockMsg').fadeOut(1000);
+                       });
+                       // End konapun additions
+               };
+
+               // plugin method for blocking element content
+               $.fn.block = function(opts) {
+                       if ( this[0] === window ) {
+                               $.blockUI( opts );
+                               return this;
+                       }
+                       var fullOpts = $.extend({}, $.blockUI.defaults, opts || 
{});
+                       this.each(function() {
+                               var $el = $(this);
+                               if (fullOpts.ignoreIfBlocked && 
$el.data('blockUI.isBlocked'))
+                                       return;
+                               $el.unblock({ fadeOut: 0 });
+                       });
+
+                       return this.each(function() {
+                               if ($.css(this,'position') == 'static') {
+                                       this.style.position = 'relative';
+                                       $(this).data('blockUI.static', true);
+                               }
+                               this.style.zoom = 1; // force 'hasLayout' in ie
+                               install(this, opts);
+                       });
+               };
+
+               // plugin method for unblocking element content
+               $.fn.unblock = function(opts) {
+                       if ( this[0] === window ) {
+                               $.unblockUI( opts );
+                               return this;
+                       }
+                       return this.each(function() {
+                               remove(this, opts);
+                       });
+               };
+
+               $.blockUI.version = 2.70; // 2nd generation blocking at no 
extra cost!
+
+               // override these in your code to change the default behavior 
and style
+               $.blockUI.defaults = {
+                       // message displayed when blocking (use null for no 
message)
+                       message:  '<h1>Please wait...</h1>',
+
+                       title: null,            // title string; only used when 
theme == true
+                       draggable: true,        // only used when theme == true 
(requires jquery-ui.js to be loaded)
+
+                       theme: false, // set to true to use with jQuery UI 
themes
+
+                       // styles for the message when blocking; if you wish to 
disable
+                       // these and use an external stylesheet then do this in 
your code:
+                       // $.blockUI.defaults.css = {};
+                       css: {
+                               padding:        0,
+                               margin:         0,
+                               width:          '30%',
+                               top:            '40%',
+                               left:           '35%',
+                               textAlign:      'center',
+                               color:          '#000',
+                               border:         '3px solid #aaa',
+                               backgroundColor:'#fff',
+                               cursor:         'wait'
+                       },
+
+                       // minimal style set used when themes are used
+                       themedCSS: {
+                               width:  '30%',
+                               top:    '40%',
+                               left:   '35%'
+                       },
+
+                       // styles for the overlay
+                       overlayCSS:  {
+                               backgroundColor:        '#000',
+                               opacity:                        0.6,
+                               cursor:                         'wait'
+                       },
+
+                       // style to replace wait cursor before unblocking to 
correct issue
+                       // of lingering wait cursor
+                       cursorReset: 'default',
+
+                       // styles applied when using $.growlUI
+                       growlCSS: {
+                               width:          '350px',
+                               top:            '10px',
+                               left:           '',
+                               right:          '10px',
+                               border:         'none',
+                               padding:        '5px',
+                               opacity:        0.6,
+                               cursor:         'default',
+                               color:          '#fff',
+                               backgroundColor: '#000',
+                               '-webkit-border-radius':'10px',
+                               '-moz-border-radius':   '10px',
+                               'border-radius':                '10px'
+                       },
+
+                       // IE issues: 'about:blank' fails on HTTPS and 
javascript:false is s-l-o-w
+                       // (hat tip to Jorge H. N. de Vasconcelos)
+                       /*jshint scripturl:true */
+                       iframeSrc: /^https/i.test(window.location.href || '') ? 
'javascript:false' : 'about:blank',
+
+                       // force usage of iframe in non-IE browsers (handy for 
blocking applets)
+                       forceIframe: false,
+
+                       // z-index for the blocking overlay
+                       baseZ: 1000,
+
+                       // set these to true to have the message automatically 
centered
+                       centerX: true, // <-- only effects element blocking 
(page block controlled via css above)
+                       centerY: true,
+
+                       // allow body element to be stetched in ie6; this makes 
blocking look better
+                       // on "short" pages.  disable if you wish to prevent 
changes to the body height
+                       allowBodyStretch: true,
+
+                       // enable if you want key and mouse events to be 
disabled for content that is blocked
+                       bindEvents: true,
+
+                       // be default blockUI will supress tab navigation from 
leaving blocking content
+                       // (if bindEvents is true)
+                       constrainTabKey: true,
+
+                       // fadeIn time in millis; set to 0 to disable fadeIn on 
block
+                       fadeIn:  200,
+
+                       // fadeOut time in millis; set to 0 to disable fadeOut 
on unblock
+                       fadeOut:  400,
+
+                       // time in millis to wait before auto-unblocking; set 
to 0 to disable auto-unblock
+                       timeout: 0,
+
+                       // disable if you don't want to show the overlay
+                       showOverlay: true,
+
+                       // if true, focus will be placed in the first available 
input field when
+                       // page blocking
+                       focusInput: true,
+
+            // elements that can receive focus
+            focusableElements: ':input:enabled:visible',
+
+                       // suppresses the use of overlay styles on FF/Linux 
(due to performance issues with opacity)
+                       // no longer needed in 2012
+                       // applyPlatformOpacityRules: true,
+
+                       // callback method invoked when fadeIn has completed 
and blocking message is visible
+                       onBlock: null,
+
+                       // callback method invoked when unblocking has 
completed; the callback is
+                       // passed the element that has been unblocked (which is 
the window object for page
+                       // blocks) and the options that were passed to the 
unblock call:
+                       //      onUnblock(element, options)
+                       onUnblock: null,
+
+                       // callback method invoked when the overlay area is 
clicked.
+                       // setting this will turn the cursor to a pointer, 
otherwise cursor defined in overlayCss will be used.
+                       onOverlayClick: null,
+
+                       // don't ask; if you really must know: 
http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
+                       quirksmodeOffsetHack: 4,
+
+                       // class name of the message block
+                       blockMsgClass: 'blockMsg',
+
+                       // if it is already blocked, then ignore it (don't 
unblock and reblock)
+                       ignoreIfBlocked: false
+               };
+
+               // private data and functions follow...
+
+               var pageBlock = null;
+               var pageBlockEls = [];
+
+               function install(el, opts) {
+                       var css, themedCSS;
+                       var full = (el == window);
+                       var msg = (opts && opts.message !== undefined ? 
opts.message : undefined);
+                       opts = $.extend({}, $.blockUI.defaults, opts || {});
+
+                       if (opts.ignoreIfBlocked && 
$(el).data('blockUI.isBlocked'))
+                               return;
+
+                       opts.overlayCSS = $.extend({}, 
$.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
+                       css = $.extend({}, $.blockUI.defaults.css, opts.css || 
{});
+                       if (opts.onOverlayClick)
+                               opts.overlayCSS.cursor = 'pointer';
+
+                       themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, 
opts.themedCSS || {});
+                       msg = msg === undefined ? opts.message : msg;
+
+                       // remove the current block (if there is one)
+                       if (full && pageBlock)
+                               remove(window, {fadeOut:0});
+
+                       // if an existing element is being used as the blocking 
content then we capture
+                       // its current place in the DOM (and current display 
style) so we can restore
+                       // it when we unblock
+                       if (msg && typeof msg != 'string' && (msg.parentNode || 
msg.jquery)) {
+                               var node = msg.jquery ? msg[0] : msg;
+                               var data = {};
+                               $(el).data('blockUI.history', data);
+                               data.el = node;
+                               data.parent = node.parentNode;
+                               data.display = node.style.display;
+                               data.position = node.style.position;
+                               if (data.parent)
+                                       data.parent.removeChild(node);
+                       }
+
+                       $(el).data('blockUI.onUnblock', opts.onUnblock);
+                       var z = opts.baseZ;
+
+                       // blockUI uses 3 layers for blocking, for simplicity 
they are all used on every platform;
+                       // layer1 is the iframe layer which is used to supress 
bleed through of underlying content
+                       // layer2 is the overlay layer which has opacity and a 
wait cursor (by default)
+                       // layer3 is the message content that is displayed 
while blocking
+                       var lyr1, lyr2, lyr3, s;
+                       if (msie || opts.forceIframe)
+                               lyr1 = $('<iframe class="blockUI" 
style="z-index:'+ (z++) 
+';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0"
 src="'+opts.iframeSrc+'"></iframe>');
+                       else
+                               lyr1 = $('<div class="blockUI" 
style="display:none"></div>');
+
+                       if (opts.theme)
+                               lyr2 = $('<div class="blockUI blockOverlay 
ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>');
+                       else
+                               lyr2 = $('<div class="blockUI blockOverlay" 
style="z-index:'+ (z++) 
+';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
+
+                       if (opts.theme && full) {
+                               s = '<div class="blockUI ' + opts.blockMsgClass 
+ ' blockPage ui-dialog ui-widget ui-corner-all" 
style="z-index:'+(z+10)+';display:none;position:fixed">';
+                               if ( opts.title ) {
+                                       s += '<div class="ui-widget-header 
ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || 
'&nbsp;')+'</div>';
+                               }
+                               s += '<div class="ui-widget-content 
ui-dialog-content"></div>';
+                               s += '</div>';
+                       }
+                       else if (opts.theme) {
+                               s = '<div class="blockUI ' + opts.blockMsgClass 
+ ' blockElement ui-dialog ui-widget ui-corner-all" 
style="z-index:'+(z+10)+';display:none;position:absolute">';
+                               if ( opts.title ) {
+                                       s += '<div class="ui-widget-header 
ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || 
'&nbsp;')+'</div>';
+                               }
+                               s += '<div class="ui-widget-content 
ui-dialog-content"></div>';
+                               s += '</div>';
+                       }
+                       else if (full) {
+                               s = '<div class="blockUI ' + opts.blockMsgClass 
+ ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
+                       }
+                       else {
+                               s = '<div class="blockUI ' + opts.blockMsgClass 
+ ' blockElement" 
style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
+                       }
+                       lyr3 = $(s);
+
+                       // if we have a message, style it
+                       if (msg) {
+                               if (opts.theme) {
+                                       lyr3.css(themedCSS);
+                                       lyr3.addClass('ui-widget-content');
+                               }
+                               else
+                                       lyr3.css(css);
+                       }
+
+                       // style the overlay
+                       if (!opts.theme /*&& 
(!opts.applyPlatformOpacityRules)*/)
+                               lyr2.css(opts.overlayCSS);
+                       lyr2.css('position', full ? 'fixed' : 'absolute');
+
+                       // make iframe layer transparent in IE
+                       if (msie || opts.forceIframe)
+                               lyr1.css('opacity',0.0);
+
+                       //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : 
el);
+                       var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') 
: $(el);
+                       $.each(layers, function() {
+                               this.appendTo($par);
+                       });
+
+                       if (opts.theme && opts.draggable && $.fn.draggable) {
+                               lyr3.draggable({
+                                       handle: '.ui-dialog-titlebar',
+                                       cancel: 'li'
+                               });
+                       }
+
+                       // ie7 must use absolute positioning in quirks mode and 
to account for activex issues (when scrolling)
+                       var expr = setExpr && (!$.support.boxModel || 
$('object,embed', full ? null : el).length > 0);
+                       if (ie6 || expr) {
+                               // give body 100% height
+                               if (full && opts.allowBodyStretch && 
$.support.boxModel)
+                                       $('html,body').css('height','100%');
+
+                               // fix ie6 issue when blocked element has a 
border width
+                               if ((ie6 || !$.support.boxModel) && !full) {
+                                       var t = sz(el,'borderTopWidth'), l = 
sz(el,'borderLeftWidth');
+                                       var fixT = t ? '(0 - '+t+')' : 0;
+                                       var fixL = l ? '(0 - '+l+')' : 0;
+                               }
+
+                               // simulate fixed position
+                               $.each(layers, function(i,o) {
+                                       var s = o[0].style;
+                                       s.position = 'absolute';
+                                       if (i < 2) {
+                                               if (full)
+                                                       
s.setExpression('height','Math.max(document.body.scrollHeight, 
document.body.offsetHeight) - 
(jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"');
+                                               else
+                                                       
s.setExpression('height','this.parentNode.offsetHeight + "px"');
+                                               if (full)
+                                                       
s.setExpression('width','jQuery.support.boxModel && 
document.documentElement.clientWidth || document.body.clientWidth + "px"');
+                                               else
+                                                       
s.setExpression('width','this.parentNode.offsetWidth + "px"');
+                                               if (fixL) 
s.setExpression('left', fixL);
+                                               if (fixT) 
s.setExpression('top', fixT);
+                                       }
+                                       else if (opts.centerY) {
+                                               if (full) 
s.setExpression('top','(document.documentElement.clientHeight || 
document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = 
document.documentElement.scrollTop ? document.documentElement.scrollTop : 
document.body.scrollTop) + "px"');
+                                               s.marginTop = 0;
+                                       }
+                                       else if (!opts.centerY && full) {
+                                               var top = (opts.css && 
opts.css.top) ? parseInt(opts.css.top, 10) : 0;
+                                               var expression = 
'((document.documentElement.scrollTop ? document.documentElement.scrollTop : 
document.body.scrollTop) + '+top+') + "px"';
+                                               
s.setExpression('top',expression);
+                                       }
+                               });
+                       }
+
+                       // show the message
+                       if (msg) {
+                               if (opts.theme)
+                                       
lyr3.find('.ui-widget-content').append(msg);
+                               else
+                                       lyr3.append(msg);
+                               if (msg.jquery || msg.nodeType)
+                                       $(msg).show();
+                       }
+
+                       if ((msie || opts.forceIframe) && opts.showOverlay)
+                               lyr1.show(); // opacity is zero
+                       if (opts.fadeIn) {
+                               var cb = opts.onBlock ? opts.onBlock : noOp;
+                               var cb1 = (opts.showOverlay && !msg) ? cb : 
noOp;
+                               var cb2 = msg ? cb : noOp;
+                               if (opts.showOverlay)
+                                       lyr2._fadeIn(opts.fadeIn, cb1);
+                               if (msg)
+                                       lyr3._fadeIn(opts.fadeIn, cb2);
+                       }
+                       else {
+                               if (opts.showOverlay)
+                                       lyr2.show();
+                               if (msg)
+                                       lyr3.show();
+                               if (opts.onBlock)
+                                       opts.onBlock.bind(lyr3)();
+                       }
+
+                       // bind key and mouse events
+                       bind(1, el, opts);
+
+                       if (full) {
+                               pageBlock = lyr3[0];
+                               pageBlockEls = 
$(opts.focusableElements,pageBlock);
+                               if (opts.focusInput)
+                                       setTimeout(focus, 20);
+                       }
+                       else
+                               center(lyr3[0], opts.centerX, opts.centerY);
+
+                       if (opts.timeout) {
+                               // auto-unblock
+                               var to = setTimeout(function() {
+                                       if (full)
+                                               $.unblockUI(opts);
+                                       else
+                                               $(el).unblock(opts);
+                               }, opts.timeout);
+                               $(el).data('blockUI.timeout', to);
+                       }
+               }
+
+               // remove the block
+               function remove(el, opts) {
+                       var count;
+                       var full = (el == window);
+                       var $el = $(el);
+                       var data = $el.data('blockUI.history');
+                       var to = $el.data('blockUI.timeout');
+                       if (to) {
+                               clearTimeout(to);
+                               $el.removeData('blockUI.timeout');
+                       }
+                       opts = $.extend({}, $.blockUI.defaults, opts || {});
+                       bind(0, el, opts); // unbind events
+
+                       if (opts.onUnblock === null) {
+                               opts.onUnblock = $el.data('blockUI.onUnblock');
+                               $el.removeData('blockUI.onUnblock');
+                       }
+
+                       var els;
+                       if (full) // crazy selector to handle odd field errors 
in ie6/7
+                               els = 
$('body').children().filter('.blockUI').add('body > .blockUI');
+                       else
+                               els = $el.find('>.blockUI');
+
+                       // fix cursor issue
+                       if ( opts.cursorReset ) {
+                               if ( els.length > 1 )
+                                       els[1].style.cursor = opts.cursorReset;
+                               if ( els.length > 2 )
+                                       els[2].style.cursor = opts.cursorReset;
+                       }
+
+                       if (full)
+                               pageBlock = pageBlockEls = null;
+
+                       if (opts.fadeOut) {
+                               count = els.length;
+                               els.stop().fadeOut(opts.fadeOut, function() {
+                                       if ( --count === 0)
+                                               reset(els,data,opts,el);
+                               });
+                       }
+                       else
+                               reset(els, data, opts, el);
+               }
+
+               // move blocking element back into the DOM where it started
+               function reset(els,data,opts,el) {
+                       var $el = $(el);
+                       if ( $el.data('blockUI.isBlocked') )
+                               return;
+
+                       els.each(function(i,o) {
+                               // remove via DOM calls so we don't lose event 
handlers
+                               if (this.parentNode)
+                                       this.parentNode.removeChild(this);
+                       });
+
+                       if (data && data.el) {
+                               data.el.style.display = data.display;
+                               data.el.style.position = data.position;
+                               data.el.style.cursor = 'default'; // #59
+                               if (data.parent)
+                                       data.parent.appendChild(data.el);
+                               $el.removeData('blockUI.history');
+                       }
+
+                       if ($el.data('blockUI.static')) {
+                               $el.css('position', 'static'); // #22
+                       }
+
+                       if (typeof opts.onUnblock == 'function')
+                               opts.onUnblock(el,opts);
+
+                       // fix issue in Safari 6 where block artifacts remain 
until reflow
+                       var body = $(document.body), w = body.width(), cssW = 
body[0].style.width;
+                       body.width(w-1).width(w);
+                       body[0].style.width = cssW;
+               }
+
+               // bind/unbind the handler
+               function bind(b, el, opts) {
+                       var full = el == window, $el = $(el);
+
+                       // don't bother unbinding if there is nothing to unbind
+                       if (!b && (full && !pageBlock || !full && 
!$el.data('blockUI.isBlocked')))
+                               return;
+
+                       $el.data('blockUI.isBlocked', b);
+
+                       // don't bind events when overlay is not in use or if 
bindEvents is false
+                       if (!full || !opts.bindEvents || (b && 
!opts.showOverlay))
+                               return;
+
+                       // bind anchors and inputs for mouse and key events
+                       var events = 'mousedown mouseup keydown keypress keyup 
touchstart touchend touchmove';
+                       if (b)
+                               $(document).bind(events, opts, handler);
+                       else
+                               $(document).unbind(events, handler);
+
+               // former impl...
+               //              var $e = $('a,:input');
+               //              b ? $e.bind(events, opts, handler) : 
$e.unbind(events, handler);
+               }
+
+               // event handler to suppress keyboard/mouse events when blocking
+               function handler(e) {
+                       // allow tab navigation (conditionally)
+                       if (e.type === 'keydown' && e.keyCode && e.keyCode == 
9) {
+                               if (pageBlock && e.data.constrainTabKey) {
+                                       var els = pageBlockEls;
+                                       var fwd = !e.shiftKey && e.target === 
els[els.length-1];
+                                       var back = e.shiftKey && e.target === 
els[0];
+                                       if (fwd || back) {
+                                               
setTimeout(function(){focus(back);},10);
+                                               return false;
+                                       }
+                               }
+                       }
+                       var opts = e.data;
+                       var target = $(e.target);
+                       if (target.hasClass('blockOverlay') && 
opts.onOverlayClick)
+                               opts.onOverlayClick(e);
+
+                       // allow events within the message content
+                       if (target.parents('div.' + opts.blockMsgClass).length 
> 0)
+                               return true;
+
+                       // allow events for content that is not being blocked
+                       return 
target.parents().children().filter('div.blockUI').length === 0;
+               }
+
+               function focus(back) {
+                       if (!pageBlockEls)
+                               return;
+                       var e = pageBlockEls[back===true ? 
pageBlockEls.length-1 : 0];
+                       if (e)
+                               e.focus();
+               }
+
+               function center(el, x, y) {
+                       var p = el.parentNode, s = el.style;
+                       var l = ((p.offsetWidth - el.offsetWidth)/2) - 
sz(p,'borderLeftWidth');
+                       var t = ((p.offsetHeight - el.offsetHeight)/2) - 
sz(p,'borderTopWidth');
+                       if (x) s.left = l > 0 ? (l+'px') : '0';
+                       if (y) s.top  = t > 0 ? (t+'px') : '0';
+               }
+
+               function sz(el, p) {
+                       return parseInt($.css(el,p),10)||0;
+               }
+
+       }
+
+
+       /*global define:true */
+       if (typeof define === 'function' && define.amd && define.amd.jQuery) {
+               define(['jquery'], setup);
+       } else {
+               setup(jQuery);
+       }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/assets/js/script.js
----------------------------------------------------------------------
diff --git a/src/main/resources/assets/js/script.js 
b/src/main/resources/assets/js/script.js
new file mode 100644
index 0000000..7e88910
--- /dev/null
+++ b/src/main/resources/assets/js/script.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ *
+ * 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.
+ */
+
+/**
+ * Created by Pmuruge on 8/28/2015.
+ */
+$(document).ready(function() {
+    $("#eval-btn").click(function() {
+        execute();
+    });
+    $('#templateAndModelForm textarea, #templateAndModelForm 
select').keydown(function (e) {
+        if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
+            execute();
+        }
+    });
+    $.blockUI.defaults.fadeIn = 1000;
+    $.blockUI.defaults.fadeOut = 0;
+});
+
+var hasPendingExecuteAjaxCall = false;
+
+function execute() {
+    if (hasPendingExecuteAjaxCall || !checkFormSendable()) {
+        return;
+    }
+    
+    var request = {
+        "template": $("#template").val(),
+        "dataModel": $("#dataModel").val(),
+        "outputFormat": $("#outputFormat").val(),
+        "locale": $("#locale").val(),
+        "timeZone": $("#timeZone").val()
+    }
+
+    $.ajax({
+        method: "POST",
+        url: "/api/execute",
+        data: JSON.stringify(request),
+        headers: { "Content-Type":"application/json" },
+        beforeSend: function (jqXHR, options) {
+            hasPendingExecuteAjaxCall = true;
+            $.blockUI({ message: null });
+            $("#error").hide();
+            return true;
+        }    
+    })
+    .done(function (data) {
+        if (data.problems && data.problems.length != 0) {
+            showResult(data.problems[0].message, true);              
+        } else {
+            showResult(data.result, false);              
+        }
+    })
+    .fail(function (data) {
+        if (data.responseJSON) {
+            showResult(data.responseJSON.errorCode + ": " + 
data.responseJSON.errorDescription, true);              
+        } else {
+            showResult("The service was unavailable or had returned an invalid 
response.", true);              
+        }
+    })
+    .always(function (data) {
+        hasPendingExecuteAjaxCall = false;
+        $.unblockUI();
+    });
+}
+
+function checkFormSendable() {
+    if($.trim($("#template").val()) === "" ) {
+        showResult("Template was empty; nothing to do.", true);
+        return false;
+    }
+    return true;
+}
+
+function showResult(result, isError) {
+    if (isError) {
+        $("#result").addClass("error");
+    } else {
+        $("#result").removeClass("error");
+    }
+    $("#result").val(result);
+    $(".resultContainer").show();
+    autosize.update($("#result"));
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/banner.txt
----------------------------------------------------------------------
diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt
new file mode 100644
index 0000000..784711d
--- /dev/null
+++ b/src/main/resources/banner.txt
@@ -0,0 +1,10 @@
+
+  ______                                  _                 ____        _ _    
        
+ |  ____|                                | |               / __ \      | (_)   
        
+ | |__ _ __ ___  ___ _ __ ___   __ _ _ __| | _____ _ __   | |  | |_ __ | |_ _ 
__   ___ 
+ |  __| '__/ _ \/ _ \ '_ ` _ \ / _` | '__| |/ / _ \ '__|  | |  | | '_ \| | | 
'_ \ / _ \
+ | |  | | |  __/  __/ | | | | | (_| | |  |   <  __/ |     | |__| | | | | | | | 
| |  __/
+ |_|  |_|  \___|\___|_| |_| |_|\__,_|_|  |_|\_\___|_|      \____/|_| |_|_|_|_| 
|_|\___|
+                                                                               
        
+                                                                               
        
+

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/freemarker-online.yml
----------------------------------------------------------------------
diff --git a/src/main/resources/freemarker-online.yml 
b/src/main/resources/freemarker-online.yml
new file mode 100644
index 0000000..370dd91
--- /dev/null
+++ b/src/main/resources/freemarker-online.yml
@@ -0,0 +1,22 @@
+# Spring configuration
+# Application Contexts to Load.
+applicationContext: ['classpath*:/spring/*-context.xml']
+
+logging:
+       # Settings for logging to a file.
+       file:
+            # If true, write log statements to a file.
+            enabled: true
+            threshold: ALL
+            # The file to which current statements will be logged.
+            currentLogFilename: 
/var/log/freemarker-online/freemarker-online.log
+
+            # When the log file rotates, the archived log will be renamed to 
this and gzipped. The
+            # %d is replaced with the previous day (yyyy-MM-dd). Custom 
rolling windows can be created
+            # by passing a SimpleDateFormat-compatible format as an argument: 
"%d{yyyy-MM-dd-hh}".
+            archivedLogFilenamePattern: 
/var/log/freemarker-online/freemarker-online-%d.log.gz
+            # The number of archived files to keep.
+            archivedFileCount: 5
+
+            # The timezone used to format dates. HINT: USE THE DEFAULT, UTC.
+            timeZone: UTC

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/spring/bootstrap-context.xml
----------------------------------------------------------------------
diff --git a/src/main/resources/spring/bootstrap-context.xml 
b/src/main/resources/spring/bootstrap-context.xml
new file mode 100644
index 0000000..2ea0c33
--- /dev/null
+++ b/src/main/resources/spring/bootstrap-context.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns:context="http://www.springframework.org/schema/context";
+       xsi:schemaLocation="http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+        http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context.xsd ">
+
+    <context:component-scan base-package="com.kenshoo"/>
+
+    <context:annotation-config/>
+
+    <!-- Use the system properties (initalized by DW) to configure spring 
context files -->
+    <bean id="propertyPlaceholderConfigurer"
+          
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
+        <property name="ignoreUnresolvablePlaceholders" value="true"/>
+        <property name="ignoreResourceNotFound" value="true"/>
+    </bean>
+
+</beans>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/view/freemarker-online.ftl
----------------------------------------------------------------------
diff --git a/src/main/resources/view/freemarker-online.ftl 
b/src/main/resources/view/freemarker-online.ftl
new file mode 100644
index 0000000..57746ff
--- /dev/null
+++ b/src/main/resources/view/freemarker-online.ftl
@@ -0,0 +1,129 @@
+<#ftl outputFormat="HTML">
+<#import "utils.ftl" as u>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <link rel="stylesheet" href="css/main.css">
+    <link rel="stylesheet" 
href="http://yui.yahooapis.com/pure/0.5.0/pure-min.css";>
+    
+    <script src="https://code.jquery.com/jquery-1.11.2.min.js";></script>
+    <script src="js/jquery.blockUI.js"></script>
+    <script src="js/autosize.min.js"></script>
+    <script src="js/script.js"></script>
+    <script>
+        $(function() {
+            // Auto-focus on first form input:
+            $('#templateAndModelForm *:input[type!=hidden]:first').focus();
+            
+            // Submit form when Ctrl+Enter is hit in a textarea:            
+
+        
+            // Dynamically adapt text areas heights to their content:
+            //$('#templateAndModelForm textarea').autosize();
+            autosize($('textarea'));
+        
+            // Show/hide data model examples:
+            $("#showHideDataModelExamples").click(function(e) {
+                $("#dataModelExamples").toggle();
+                        $("#hideDataModelExamplesLabel").toggle();
+                 $("#showDataModelExamplesLabel").toggle();
+                
+                 e.preventDefault();
+                return false;
+            })
+            <#if execute>
+                execute();
+            </#if>
+        });
+    </script>
+    
+    <title>Online FreeMarker Template Tester</title>
+</head>
+<body>
+<div id="layout">
+    <div id="main">
+        <div class="header">
+            <h1>Online FreeMarker Template Tester</h1>
+        </div>
+
+        <div class="content">
+            <!--[if lte IE 8]>
+            <div style="background-color: #C00; color: #fff; padding: 12px 
24px;">
+              You seem to use Internet Explorer 8 or older. This page might 
won't work properly with that.
+            </div>
+            <![endif]-->
+          
+            <form id="templateAndModelForm" method="post" class="pure-form 
pure-form-stacked">
+                <label for="template">Template <span class="faint">(Apache 
FreeMarker ${freeMarkerVersion})</span></label>
+                <textarea id="template" name="template" class="pure-input-1 
source-code"
+                        placeholder="Enter template, like: Hello 
${r'${user}'}!"
+                >${template}</textarea>
+    
+                <label for="template">
+                    Data model
+                    (<a id="showHideDataModelExamples" href="#" 
tabindex="-1"><!--
+                    --><span id="showDataModelExamplesLabel">show</span><!--
+                    --><span id="hideDataModelExamplesLabel" 
class="hiddenByDefault">hide</span>
+                    examples</a>)
+                </label>
+                <div id="dataModelExamples" class="hiddenByDefault">
+                  <div class="description">
+                      Note: This syntax is specific to this online service; 
normally, you just have Java objects as
+                      data-model.
+                  </div>
+                  <pre>someString = Some value
+otherString = "JSON\nsyntax"
+someNumber = 3.14
+someBoolean = true
+someDate = 2014-02-28
+someTime = 20:50:30.5+02:00
+someDatetime = 2014-02-28T18:50Z
+someList = ["JSON", "syntax", 1, 2, 3 ]
+someMap = { "JSON syntax": true, "nestedList": [1, 2, 3] }
+someXML = &lt;example x="1"&gt;text&lt;/example&gt;</pre></div>
+                <textarea id="dataModel" name="dataModel" class="pure-input-1 
source-code"
+                        placeholder='Enter one or more assignments (e.g., user 
= John Doe), starting each in its own line.'
+                >${dataModel}</textarea>
+                <div class="formPanel">
+                  <div class="horizontalBox">
+                    <@u.htmlSelect caption="Output format" name="outputFormat" 
selectionOptions=outputFormats />
+                  </div>
+                  <div class="horizontalBox">
+                    <@u.htmlSelect caption="Locale" name="locale" 
selectionOptions=locales />
+                  </div>
+                  <div class="horizontalBox">
+                    <@u.htmlSelect caption="Time zone" name="timeZone" 
selectionOptions=timeZones />
+                  </div>
+                </div>
+                <div class="formBottomButtonsContainer">
+                       <input id="eval-btn" type="button" value="Evaluate" 
class="pure-button pure-button-primary"/>
+                       &nbsp; <span class="faint">Ctrl+Enter in input fields 
will submit this form too</span>
+                </div>
+                <div style="display:none" class="resultContainer">
+                    <label for="result">Result</label>
+                    <textarea id="result" class="pure-input-1 source-code" 
readonly></textarea>
+                </div>
+
+            </form>
+        </div><!-- content -->
+        
+        <div class="footer">
+            FreeMarker documentation:
+            <a href="http://freemarker.org/docs/"; target="_blank">Contents</a>
+            |
+            <a 
href="http://freemarker.org/docs/dgui_template_overallstructure.html"; 
target="_blank">Overall&nbsp;syntax</a>
+            |
+            <a 
href="http://freemarker.org/docs/dgui_template_exp.html#exp_cheatsheet"; 
target="_blank">Expression&nbsp;syntax</a>
+            |
+            <a href="http://freemarker.org/docs/ref_directive_alphaidx.html"; 
target="_blank">List&nbsp;of&nbsp;&lt;#<i>directives</i>&gt;</a>
+            |
+            <a href="http://freemarker.org/docs/ref_builtins_alphaidx.html"; 
target="_blank">List&nbsp;of&nbsp;<tt>?<i>built_in</i></tt> functions</a>
+        </div><!-- footer -->
+    </div><!-- main -->
+    
+    <!-- Fork me on GitHub: -->
+    <a href="https://github.com/kenshoo/freemarker-online"; 
target="_blank"><img style="position: absolute; top: 0; right: 0; border: 0;" 
src="https://camo.githubusercontent.com/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67";
 alt="Fork me on GitHub" 
data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png";></a>
+</div><!-- layout -->
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/main/resources/view/utils.ftl
----------------------------------------------------------------------
diff --git a/src/main/resources/view/utils.ftl 
b/src/main/resources/view/utils.ftl
new file mode 100644
index 0000000..cfa5ee6
--- /dev/null
+++ b/src/main/resources/view/utils.ftl
@@ -0,0 +1,13 @@
+<#ftl outputFormat='HTML'>
+
+<#macro htmlSelect caption name selectionOptions>
+  <div>${caption}:</div>
+  <div> 
+    <select name="${name}" id="${name}" class="pure-input-1">
+      <#list selectionOptions as selectionOption>
+        <#local value = selectionOption.value>
+        <option value="${value}"<#if value == .vars[name]!> 
selected</#if>>${selectionOption.label}</option>
+      </#list>
+    </select>
+  </div>
+</#macro>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/test/java/com/kenshoo/freemarker/platform/DropWizardServiceTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/com/kenshoo/freemarker/platform/DropWizardServiceTest.java 
b/src/test/java/com/kenshoo/freemarker/platform/DropWizardServiceTest.java
new file mode 100644
index 0000000..38f7e88
--- /dev/null
+++ b/src/test/java/com/kenshoo/freemarker/platform/DropWizardServiceTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.platform;
+
+import com.google.common.io.Resources;
+import com.kenshoo.freemarker.dropwizard.ApplicationStartup;
+import com.yammer.dropwizard.testing.junit.DropwizardServiceRule;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: shlomis
+ * Date: 9/9/13
+ * Time: 10:43 AM
+ */
+public class DropWizardServiceTest {
+    @ClassRule
+    public static TestRule testRule = new 
DropwizardServiceRule<>(ApplicationStartup.class,
+            Resources.getResource("freemarker-online.yml").getPath());
+
+
+    @Test
+    public void testServerIsUp() throws Exception {
+        ((DropwizardServiceRule) testRule).getService();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/test/java/com/kenshoo/freemarker/platform/YamlPropertiesPersister.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/com/kenshoo/freemarker/platform/YamlPropertiesPersister.java 
b/src/test/java/com/kenshoo/freemarker/platform/YamlPropertiesPersister.java
new file mode 100644
index 0000000..e01a881
--- /dev/null
+++ b/src/test/java/com/kenshoo/freemarker/platform/YamlPropertiesPersister.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ * 
+ * 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 com.kenshoo.freemarker.platform;
+
+import com.fasterxml.jackson.dataformat.yaml.snakeyaml.Yaml;
+import org.springframework.util.PropertiesPersister;
+import org.springframework.util.StringUtils;
+
+import java.io.*;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: shlomis
+ * Date: 9/8/13
+ * Time: 10:50 PM
+ */
+public class YamlPropertiesPersister implements PropertiesPersister {
+    @Override
+    public void load(Properties props, InputStream is) throws IOException {
+        load(props, new InputStreamReader(is));
+    }
+
+    /**
+     * We want to traverse map representing Yaml object and each time we find 
String=String pair we want to
+     * save it as Property. As we are going deeper into map we generate 
compound key as path-like String
+     *
+     * @see 
org.springframework.util.PropertiesPersister#load(java.util.Properties, 
java.io.Reader)
+     */
+    @Override
+    public void load(Properties props, Reader reader) throws IOException {
+        Yaml yaml = new Yaml();
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map = (Map<String, Object>) yaml.load(reader);
+        // now we can populate supplied props
+        assignProperties(props, map, null);
+    }
+
+    @SuppressWarnings("unchecked")
+    public void assignProperties(Properties props, Map<String, Object> map, 
String path) {
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            String key = entry.getKey();
+            if (!StringUtils.isEmpty(path))
+                key = path + "." + key;
+            Object val = entry.getValue();
+            if (val instanceof String) {
+                // see if we need to create a compound key
+                props.put(key, val);
+            } else if (val instanceof Map) {
+                assignProperties(props, (Map<String, Object>) val, key);
+            }
+        }
+    }
+
+    @Override
+    public void store(Properties props, OutputStream os, String header) throws 
IOException {
+        throw new IllegalStateException("Current implementation is a 
read-only");
+    }
+
+    @Override
+    public void store(Properties props, Writer writer, String header) throws 
IOException {
+        throw new IllegalStateException("Current implementation is a 
read-only");
+    }
+
+    @Override
+    public void loadFromXml(Properties props, InputStream is) throws 
IOException {
+        throw new IllegalStateException("Use DefaultPropertiesPersister if you 
want to read/write XML");
+    }
+
+    @Override
+    public void storeToXml(Properties props, OutputStream os, String header) 
throws IOException {
+        throw new IllegalStateException("Use DefaultPropertiesPersister if you 
want to load/store to XML");
+    }
+
+    @Override
+    public void storeToXml(Properties props, OutputStream os, String header, 
String encoding) throws IOException {
+        throw new IllegalStateException("Use DefaultPropertiesPersister if you 
want to read/write XML");
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker-online-tester/blob/abb26297/src/test/java/com/kenshoo/freemarker/resources/FreeMarkerOnlineExecuteResourceTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/com/kenshoo/freemarker/resources/FreeMarkerOnlineExecuteResourceTest.java
 
b/src/test/java/com/kenshoo/freemarker/resources/FreeMarkerOnlineExecuteResourceTest.java
new file mode 100644
index 0000000..f4886c8
--- /dev/null
+++ 
b/src/test/java/com/kenshoo/freemarker/resources/FreeMarkerOnlineExecuteResourceTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2014 Kenshoo.com
+ *
+ * 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 com.kenshoo.freemarker.resources;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.springframework.web.context.ContextLoaderListener;
+import org.springframework.web.context.request.RequestContextListener;
+
+import com.kenshoo.freemarker.model.ExecuteRequest;
+import com.kenshoo.freemarker.model.ExecuteResourceField;
+import com.kenshoo.freemarker.model.ExecuteResourceProblem;
+import com.kenshoo.freemarker.model.ExecuteResponse;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.spi.spring.container.servlet.SpringServlet;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+
+/**
+ * Created by Pmuruge on 8/29/2015.
+ */
+public class FreeMarkerOnlineExecuteResourceTest extends JerseyTest {
+    private static final String DATA_MODEL = "user=John";
+    private static final String TEMPLATE_WITH_VARIABLE = "Welcome ${user}";
+    private static final String TEMPLATE_PLAIN = "Welcome John";
+    private static final String MALFORMED_DATA_MODEL = "userJohn";
+    private static final String EXECUTE_API = "api/execute";
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("com.kenshoo.freemarker.resources")
+                        .contextPath("/")
+                        .contextListenerClass(ContextLoaderListener.class)
+                        .contextParam("contextConfigLocation", 
"classpath:spring/bootstrap-context.xml")
+                        .servletClass(SpringServlet.class)
+                        .requestListenerClass(RequestContextListener.class)
+                        .build();
+    }
+
+    @Test
+    public void testSuccessRequest() throws Exception {
+        ExecuteRequest req = new ExecuteRequest(TEMPLATE_WITH_VARIABLE, 
DATA_MODEL);
+        ClientResponse resp = client().resource(getBaseURI().toString() + 
EXECUTE_API)
+                .header("Content-Type", 
"application/json").entity(req).post(ClientResponse.class);
+        assertEquals(200, resp.getStatus());
+        ExecuteResponse response = resp.getEntity(ExecuteResponse.class);
+        assertNull(response.getProblems());
+    }
+
+    @Test
+    public void testMalformedDataModel() throws Exception {
+        ExecuteRequest req = new ExecuteRequest(TEMPLATE_PLAIN, 
MALFORMED_DATA_MODEL);
+        ClientResponse resp = client().resource(getBaseURI().toString() + 
EXECUTE_API)
+                .header("Content-Type", 
"application/json").entity(req).post(ClientResponse.class);
+        assertEquals(200, resp.getStatus());
+        ExecuteResponse response = resp.getEntity(ExecuteResponse.class);
+        assertNotNull(response.getProblems());
+        assertTrue(containsProblem(response, ExecuteResourceField.DATA_MODEL));
+    }
+
+    @Test
+    public void testLongDataModel() throws Exception {
+        ExecuteRequest req = new ExecuteRequest(TEMPLATE_PLAIN, 
create30KString());
+        ClientResponse resp = client().resource(getBaseURI().toString() + 
EXECUTE_API)
+                .header("Content-Type", 
"application/json").entity(req).post(ClientResponse.class);
+        assertEquals(200, resp.getStatus());
+        ExecuteResponse response = resp.getEntity(ExecuteResponse.class);
+        assertNotNull(response.getProblems());
+        assertTrue(containsProblem(response, ExecuteResourceField.DATA_MODEL));
+        String problemMessage = getProblemMessage(response, 
ExecuteResourceField.DATA_MODEL);
+        assertThat(problemMessage, containsString("data model"));
+        assertThat(problemMessage, containsString("limit"));
+    }
+
+    @Test
+    public void testLongTemplate() throws Exception {
+        ExecuteRequest req = new ExecuteRequest(create30KString(), DATA_MODEL);
+        ClientResponse resp = client().resource(getBaseURI().toString() + 
EXECUTE_API)
+                .header("Content-Type", 
"application/json").entity(req).post(ClientResponse.class);
+        assertEquals(200, resp.getStatus());
+        ExecuteResponse response = resp.getEntity(ExecuteResponse.class);
+        assertNotNull(response.getProblems());
+        assertTrue(containsProblem(response, ExecuteResourceField.TEMPLATE));
+        String problemMessage = getProblemMessage(response, 
ExecuteResourceField.TEMPLATE);
+        assertThat(problemMessage, containsString("template"));
+        assertThat(problemMessage, containsString("limit"));
+    }
+
+    @Test
+    public void testMultipleErrorsDataModel() throws Exception {
+        ExecuteRequest req = new ExecuteRequest(create30KString(), 
create30KString());
+        req.setOutputFormat("wrongOutputFormat");
+        req.setLocale("wrongLocale");
+        req.setTimeZone("wrongTimeZone");
+        
+        ClientResponse resp = client().resource(getBaseURI() + EXECUTE_API)
+                .header("Content-Type", 
"application/json").entity(req).post(ClientResponse.class);
+        
+        assertEquals(200, resp.getStatus());
+        ExecuteResponse response = resp.getEntity(ExecuteResponse.class);
+        assertNotNull(response.getProblems());
+        assertThat(getProblemMessage(response, ExecuteResourceField.TEMPLATE), 
containsString("limit"));
+        assertThat(getProblemMessage(response, 
ExecuteResourceField.DATA_MODEL), containsString("limit"));
+        assertThat(getProblemMessage(response, 
ExecuteResourceField.OUTPUT_FORMAT), containsString("wrongOutputFormat"));
+        assertThat(getProblemMessage(response, ExecuteResourceField.LOCALE), 
containsString("wrongLocale"));
+        assertThat(getProblemMessage(response, 
ExecuteResourceField.TIME_ZONE), containsString("wrongTimeZone"));
+    }
+    
+    private String create30KString() {
+        final String veryLongString;
+        {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < 30000 / 10; i++) {
+                sb.append("0123456789");
+            }
+            veryLongString = sb.toString();
+        }
+        return veryLongString;
+    }
+
+    private boolean containsProblem(ExecuteResponse response, 
ExecuteResourceField field) {
+        for (ExecuteResourceProblem problem : response.getProblems()) {
+            if (problem.getField() == field) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String getProblemMessage(ExecuteResponse response, 
ExecuteResourceField field) {
+        for (ExecuteResourceProblem problem : response.getProblems()) {
+            if (problem.getField() == field) {
+                return problem.getMessage();
+            }
+        }
+        return null;
+    }
+    
+}


Reply via email to