FREEMARKER-55: Adding skeletal form directive
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/1f7100ce Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/1f7100ce Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/1f7100ce Branch: refs/heads/3 Commit: 1f7100cee9ca2e1c2c28e4bc5bded6e4857677ec Parents: aa264af Author: Woonsan Ko <[email protected]> Authored: Sat Dec 30 01:07:12 2017 -0500 Committer: Woonsan Ko <[email protected]> Committed: Sat Dec 30 01:07:12 2017 -0500 ---------------------------------------------------------------------- ...stractHtmlElementTemplateDirectiveModel.java | 1 + .../model/form/FormTemplateDirectiveModel.java | 346 +++++++++++++++++++ .../SpringFormTemplateCallableHashModel.java | 1 + .../form/FormTemplateDirectiveModelTest.java | 68 ++++ .../test/model/form/form-directive-usages.ftlh | 42 +++ 5 files changed, 458 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1f7100ce/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractHtmlElementTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractHtmlElementTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractHtmlElementTemplateDirectiveModel.java index 1bd8d5c..f711f3a 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractHtmlElementTemplateDirectiveModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractHtmlElementTemplateDirectiveModel.java @@ -107,6 +107,7 @@ public abstract class AbstractHtmlElementTemplateDirectiveModel private static final int CSSERRORCLASS_PARAM_IDX = NAMED_ARGS_OFFSET + 16; private static final String CSSERRORCLASS_PARAM_NAME = "cssErrorClass"; + // TODO: It's a problem to see NAMED_ARGS_ENTRY_LIST is visible from child classes! @SuppressWarnings("unchecked") protected static List<StringToIndexMap.Entry> NAMED_ARGS_ENTRY_LIST = _CollectionUtils.mergeImmutableLists(false, http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1f7100ce/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java new file mode 100644 index 0000000..565245b --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java @@ -0,0 +1,346 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.freemarker.spring.model.form; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.freemarker.core.CallPlace; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.CallableUtils; +import org.apache.freemarker.core.util.StringToIndexMap; +import org.apache.freemarker.core.util._CollectionUtils; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.support.RequestContext; +import org.springframework.web.servlet.support.RequestDataValueProcessor; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.UriUtils; + +public class FormTemplateDirectiveModel extends AbstractHtmlElementTemplateDirectiveModel { + + public static final String NAME = "form"; + + private static final int NAMED_ARGS_OFFSET = AbstractHtmlElementTemplateDirectiveModel.NAMED_ARGS_ENTRY_LIST.size() + + 1; + + private static final int ACTION_PARAM_IDX = NAMED_ARGS_OFFSET; + private static final String ACTION_PARAM_NAME = "action"; + + private static final int METHOD_PARAM_IDX = NAMED_ARGS_OFFSET + 1; + private static final String METHOD_PARAM_NAME = "method"; + + private static final int TARGET_PARAM_IDX = NAMED_ARGS_OFFSET + 2; + private static final String TARGET_PARAM_NAME = "target"; + + private static final int ENCTYPE_PARAM_IDX = NAMED_ARGS_OFFSET + 3; + private static final String ENCTYPE_PARAM_NAME = "enctype"; + + private static final int ACCEPT_CHARSET_PARAM_IDX = NAMED_ARGS_OFFSET + 4; + private static final String ACCEPT_CHARSET_PARAM_NAME = "accept-charset"; + + private static final int ONSUBMIT_PARAM_IDX = NAMED_ARGS_OFFSET + 5; + private static final String ONSUBMIT_PARAM_NAME = "onsubmit"; + + private static final int ONRESET_PARAM_IDX = NAMED_ARGS_OFFSET + 6; + private static final String ONRESET_PARAM_NAME = "onreset"; + + private static final int AUTOCOMPLETE_PARAM_IDX = NAMED_ARGS_OFFSET + 7; + private static final String AUTOCOMPLETE_PARAM_NAME = "autocomplete"; + + private static final int NAME_PARAM_IDX = NAMED_ARGS_OFFSET + 8; + private static final String NAME_PARAM_NAME = "name"; + + private static final int VALUE_PARAM_IDX = NAMED_ARGS_OFFSET + 9; + private static final String VALUE_PARAM_NAME = "value"; + + private static final int TYPE_PARAM_IDX = NAMED_ARGS_OFFSET + 10; + private static final String TYPE_PARAM_NAME = "type"; + + private static final int SERVLET_RELATIVE_ACTION_PARAM_IDX = NAMED_ARGS_OFFSET + 11; + private static final String SERVLET_RELATIVE_ACTION_PARAM_NAME = "servletRelativeAction"; + + private static final int METHOD_PARAM_PARAM_IDX = NAMED_ARGS_OFFSET + 12; + private static final String METHOD_PARAM_PARAM_NAME = "methodParam"; + + @SuppressWarnings("unchecked") + protected static List<StringToIndexMap.Entry> NAMED_ARGS_ENTRY_LIST = + _CollectionUtils.mergeImmutableLists(false, + AbstractHtmlElementTemplateDirectiveModel.NAMED_ARGS_ENTRY_LIST, + Arrays.asList( + new StringToIndexMap.Entry(ACTION_PARAM_NAME, ACTION_PARAM_IDX), + new StringToIndexMap.Entry(METHOD_PARAM_NAME, METHOD_PARAM_IDX), + new StringToIndexMap.Entry(TARGET_PARAM_NAME, TARGET_PARAM_IDX), + new StringToIndexMap.Entry(ENCTYPE_PARAM_NAME, ENCTYPE_PARAM_IDX), + new StringToIndexMap.Entry(ACCEPT_CHARSET_PARAM_NAME, ACCEPT_CHARSET_PARAM_IDX), + new StringToIndexMap.Entry(ONSUBMIT_PARAM_NAME, ONSUBMIT_PARAM_IDX), + new StringToIndexMap.Entry(ONRESET_PARAM_NAME, ONRESET_PARAM_IDX), + new StringToIndexMap.Entry(AUTOCOMPLETE_PARAM_NAME, AUTOCOMPLETE_PARAM_IDX), + new StringToIndexMap.Entry(NAME_PARAM_NAME, NAME_PARAM_IDX), + new StringToIndexMap.Entry(VALUE_PARAM_NAME, VALUE_PARAM_IDX), + new StringToIndexMap.Entry(TYPE_PARAM_NAME, TYPE_PARAM_IDX), + new StringToIndexMap.Entry(SERVLET_RELATIVE_ACTION_PARAM_NAME, SERVLET_RELATIVE_ACTION_PARAM_IDX), + new StringToIndexMap.Entry(METHOD_PARAM_PARAM_NAME, METHOD_PARAM_PARAM_IDX) + ) + ); + + private static final ArgumentArrayLayout ARGS_LAYOUT = + ArgumentArrayLayout.create( + 1, + false, + StringToIndexMap.of(NAMED_ARGS_ENTRY_LIST.toArray(new StringToIndexMap.Entry[NAMED_ARGS_ENTRY_LIST.size()])), + true + ); + + private static final String FORM_TAG_NAME = "form"; + + private static final String INPUT_TAG_NAME = "input"; + + private static final String DEFAULT_METHOD = "post"; + + private String action; + private String method; + private String target; + private String enctype; + private String acceptCharset; + private String onsubmit; + private String onreset; + private String autocomplete; + private String name; + private String value; + private String type; + private String servletRelativeAction; + private String methodParam; + + protected FormTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + @Override + public boolean isNestedContentSupported() { + return true; + } + + @Override + public ArgumentArrayLayout getDirectiveArgumentArrayLayout() { + return ARGS_LAYOUT; + } + + @Override + protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env, + ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) + throws TemplateException, IOException { + + super.executeInternal(args, callPlace, out, env, objectWrapperAndUnwrapper, requestContext); + + action = CallableUtils.getOptionalStringArgument(args, ACTION_PARAM_IDX, "", this); + method = CallableUtils.getOptionalStringArgument(args, METHOD_PARAM_IDX, DEFAULT_METHOD, this); + target = CallableUtils.getOptionalStringArgument(args, TARGET_PARAM_IDX, this); + enctype = CallableUtils.getOptionalStringArgument(args, ENCTYPE_PARAM_IDX, this); + acceptCharset = CallableUtils.getOptionalStringArgument(args, ACCEPT_CHARSET_PARAM_IDX, this); + onsubmit = CallableUtils.getOptionalStringArgument(args, ONSUBMIT_PARAM_IDX, this); + onreset = CallableUtils.getOptionalStringArgument(args, ONRESET_PARAM_IDX, this); + autocomplete = CallableUtils.getOptionalStringArgument(args, AUTOCOMPLETE_PARAM_IDX, this); + name = CallableUtils.getOptionalStringArgument(args, NAME_PARAM_IDX, this); + value = CallableUtils.getOptionalStringArgument(args, VALUE_PARAM_IDX, this); + type = CallableUtils.getOptionalStringArgument(args, TYPE_PARAM_IDX, this); + servletRelativeAction = CallableUtils.getOptionalStringArgument(args, SERVLET_RELATIVE_ACTION_PARAM_IDX, this); + methodParam = CallableUtils.getOptionalStringArgument(args, METHOD_PARAM_PARAM_IDX, this); + + TagOutputter tagOut = new TagOutputter(out); + + tagOut.beginTag(FORM_TAG_NAME); + writeDefaultAttributes(tagOut); + tagOut.writeAttribute(ACTION_PARAM_NAME, resolveAction(env)); + writeOptionalAttribute(tagOut, METHOD_PARAM_NAME, getHttpMethod()); + writeOptionalAttribute(tagOut, TARGET_PARAM_NAME, getTarget()); + writeOptionalAttribute(tagOut, ENCTYPE_PARAM_NAME, getEnctype()); + writeOptionalAttribute(tagOut, ACCEPT_CHARSET_PARAM_NAME, getAcceptCharset()); + writeOptionalAttribute(tagOut, ONSUBMIT_PARAM_NAME, getOnsubmit()); + writeOptionalAttribute(tagOut, ONRESET_PARAM_NAME, getOnreset()); + writeOptionalAttribute(tagOut, AUTOCOMPLETE_PARAM_NAME, getAutocomplete()); + + tagOut.forceBlock(); + + final String methodName = getMethod(); + + if (!isMethodBrowserSupported(methodName)) { + if (!isValidHttpMethod(methodName)) { + throw new IllegalArgumentException("Invalid HTTP method: " + method); + } + + String inputName = getMethodParam(); + String inputType = "hidden"; + tagOut.beginTag(INPUT_TAG_NAME); + writeOptionalAttribute(tagOut, TYPE_PARAM_NAME, inputType); + writeOptionalAttribute(tagOut, NAME_PARAM_NAME, inputName); + writeOptionalAttribute(tagOut, VALUE_PARAM_NAME, processFieldValue(env, inputName, methodName, inputType)); + tagOut.endTag(); + } + + // TODO: expose the form object name for nested tags... + + // TODO: save previous nestedPath value, build and expose current nestedPath value. + + } + + protected String getModelAttribute() { + return getPath(); + } + + public String getAction() { + return action; + } + + public String getMethod() { + return method; + } + + public String getTarget() { + return target; + } + + public String getEnctype() { + return enctype; + } + + public String getAcceptCharset() { + return acceptCharset; + } + + public String getOnsubmit() { + return onsubmit; + } + + public String getOnreset() { + return onreset; + } + + public String getAutocomplete() { + return autocomplete; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public String getType() { + return type; + } + + public String getServletRelativeAction() { + return servletRelativeAction; + } + + public String getMethodParam() { + return methodParam; + } + + protected boolean isMethodBrowserSupported(String method) { + return ("get".equalsIgnoreCase(method) || "post".equalsIgnoreCase(method)); + } + + protected String resolveAction(Environment env) throws TemplateException { + RequestContext requestContext = getRequestContext(env, false); + String action = getAction(); + String servletRelativeAction = getServletRelativeAction(); + + if (StringUtils.hasText(action)) { + action = getDisplayString(evaluate(ACTION_PARAM_NAME, action), false); + return processAction(env, action); + } else if (StringUtils.hasText(servletRelativeAction)) { + String pathToServlet = requestContext.getPathToServlet(); + + if (servletRelativeAction.startsWith("/") && + !servletRelativeAction.startsWith(requestContext.getContextPath())) { + servletRelativeAction = pathToServlet + servletRelativeAction; + } + + servletRelativeAction = getDisplayString(evaluate(ACTION_PARAM_NAME, servletRelativeAction), false); + return processAction(env, servletRelativeAction); + } else { + String requestUri = requestContext.getRequestUri(); + String encoding = getResponse().getCharacterEncoding(); + + try { + requestUri = UriUtils.encodePath(requestUri, encoding); + } catch (UnsupportedEncodingException ex) { + // shouldn't happen - if it does, proceed with requestUri as-is + } + + HttpServletResponse response = getResponse(); + + if (response != null) { + requestUri = response.encodeURL(requestUri); + String queryString = requestContext.getQueryString(); + + if (StringUtils.hasText(queryString)) { + requestUri += "?" + HtmlUtils.htmlEscape(queryString); + } + } + + if (StringUtils.hasText(requestUri)) { + return processAction(env, requestUri); + } else { + throw new IllegalArgumentException("Attribute 'action' is required. " + + "Attempted to resolve against current request URI but request URI was null."); + } + } + } + + private String getHttpMethod() { + final String methodName = getMethod(); + return (isMethodBrowserSupported(methodName) ? methodName : DEFAULT_METHOD); + } + + private boolean isValidHttpMethod(String method) { + for (HttpMethod httpMethod : HttpMethod.values()) { + if (httpMethod.name().equalsIgnoreCase(method)) { + return true; + } + } + + return false; + } + + private String processAction(Environment env, String action) throws TemplateException { + RequestDataValueProcessor processor = getRequestContext(env, false).getRequestDataValueProcessor(); + HttpServletRequest request = getRequest(); + if (processor != null && request != null) { + action = processor.processAction((HttpServletRequest) request, action, getHttpMethod()); + } + return action; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1f7100ce/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java index aeec2bb..3c9becd 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java @@ -46,6 +46,7 @@ public final class SpringFormTemplateCallableHashModel implements TemplateHashMo private final Map<String, TemplateModel> modelsMap = new HashMap<>(); public SpringFormTemplateCallableHashModel(final HttpServletRequest request, final HttpServletResponse response) { + modelsMap.put(FormTemplateDirectiveModel.NAME, new FormTemplateDirectiveModel(request, response)); modelsMap.put(InputTemplateDirectiveModel.NAME, new InputTemplateDirectiveModel(request, response)); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1f7100ce/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModelTest.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModelTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModelTest.java new file mode 100644 index 0000000..2795171 --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModelTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.freemarker.spring.model.form; + +import org.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration("classpath:META-INF/web-resources") +@ContextConfiguration(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class FormTemplateDirectiveModelTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testBasicUsages() throws Exception { + final Long userId = userRepository.getUserIds().iterator().next(); + final User user = userRepository.getUser(userId); + mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/form/form-directive-usages") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1f7100ce/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/form-directive-usages.ftlh ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/form-directive-usages.ftlh b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/form-directive-usages.ftlh new file mode 100644 index 0000000..2338ed2 --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/form-directive-usages.ftlh @@ -0,0 +1,42 @@ +<#-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<html> +<body> + + <h1>Form 1</h1> + <hr/> + <@spring.form.form "user"> + <table> + <tr> + <th>First name:</th> + <td> + <@spring.form.input 'user.firstName' /> + </td> + </tr> + <tr> + <th>Last name:</th> + <td> + <@spring.form.input 'user.lastName' /> + </td> + </tr> + </table> + </@spring.form.form> + +</body> +</html>
