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 || ' ')+'</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 || ' ')+'</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 = <example x="1">text</example></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"/> + <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 syntax</a> + | + <a href="http://freemarker.org/docs/dgui_template_exp.html#exp_cheatsheet" target="_blank">Expression syntax</a> + | + <a href="http://freemarker.org/docs/ref_directive_alphaidx.html" target="_blank">List of <#<i>directives</i>></a> + | + <a href="http://freemarker.org/docs/ref_builtins_alphaidx.html" target="_blank">List of <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; + } + +}
