/* * Copyright 2017 OmniFaces * * 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 org.omnifaces.renderkit; import static org.omnifaces.util.Components.getCurrentComponent; import static org.omnifaces.util.Faces.getInitParameter; import static org.omnifaces.util.Utils.isEmpty; import static org.omnifaces.util.Utils.isOneInstanceOf; import static org.omnifaces.util.Utils.unmodifiableSet; import java.io.IOException; import java.io.Writer; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.faces.component.UICommand; import javax.faces.component.UIComponent; import javax.faces.component.UIForm; import javax.faces.component.UIInput; import javax.faces.component.UISelectBoolean; import javax.faces.component.UISelectMany; import javax.faces.component.UISelectOne; import javax.faces.component.html.HtmlCommandButton; import javax.faces.component.html.HtmlInputSecret; import javax.faces.component.html.HtmlInputText; import javax.faces.component.html.HtmlInputTextarea; import javax.faces.context.ResponseWriter; import javax.faces.context.ResponseWriterWrapper; import javax.faces.render.RenderKit; import javax.faces.render.RenderKitWrapper; /** * <p> * This HTML5 render kit adds support for HTML5 specific attributes which are unsupported by the JSF {@link UIForm}, * {@link UIInput} and {@link UICommand} components. So far in JSF 2.0 and 2.1 only the <code>autocomplete</code> * attribute is supported in {@link UIInput} components. All other attributes are by design ignored by the JSF standard * HTML render kit. This HTML5 render kit supports the following HTML5 specific attributes: * <ul> * <li>{@link UIForm}: <ul><li><code>autocomplete</code></li></ul></li> * <li>{@link UISelectBoolean}, {@link UISelectOne} and {@link UISelectMany}: <ul><li><code>autofocus</code></li></ul></li> * <li>{@link HtmlInputText}: <ul><li><code>type</code> (supported values are <code>text</code> (default), <code>search</code>, <code>email</code>, <code>url</code>, <code>tel</code>, <code>range</code>, <code>number</code> and <code>date</code>)</li><li><code>autofocus</code></li><li><code>list</code></li><li><code>pattern</code></li><li><code>placeholder</code></li><li><code>spellcheck</code></li><li><code>min</code></li><li><code>max</code></li><li><code>step</code></li></ul>(the latter three are only supported on <code>type</code> of <code>range</code>, <code>number</code> and <code>date</code>)</li> * <li>{@link HtmlInputTextarea}: <ul><li><code>autofocus</code></li><li><code>maxlength</code></li><li><code>placeholder</code></li><li><code>spellcheck</code></li><li><code>wrap</code></li></ul></li> * <li>{@link HtmlInputSecret}: <ul><li><code>autofocus</code></li><li><code>pattern</code></li><li><code>placeholder</code></li></ul></li> * <li>{@link HtmlCommandButton}: <ul><li><code>autofocus</code></li></ul></li> * </ul> * <p> * Note: the <code>list</code> attribute expects a <code><datalist></code> element which needs to be coded in * "plain vanilla" HTML (and is currently, July 2014, only supported in IE 10, Firefox 4, Chrome 20 and Opera 11). See * also <a href="http://www.html5tutorial.info/html5-datalist.php">HTML5 tutorial</a>. * * <h3>Installation</h3> * <p> * To use the HTML5 render kit, register it as follows in <code>faces-config.xml</code>: * <pre> * <factory> * <render-kit-factory>org.omnifaces.renderkit.Html5RenderKitFactory</render-kit-factory> * </factory> * </pre> * * <h3>Configuration</h3> * <p> * You can also configure additional passthrough attributes via the * {@value org.omnifaces.renderkit.Html5RenderKit#PARAM_NAME_PASSTHROUGH_ATTRIBUTES} context parameter in * <code>web.xml</code>, wherein the passthrough attributes are been specified in semicolon-separated * <code>com.example.SomeComponent=attr1,attr2,attr3</code> key=value pairs. The key represents the fully qualified * name of a class whose {@link Class#isInstance(Object)} must return <code>true</code> for the particular component * and the value represents the commaseparated string of names of passthrough attributes. Here's an example: * <pre> * <context-param> * <param-name>org.omnifaces.HTML5_RENDER_KIT_PASSTHROUGH_ATTRIBUTES</param-name> * <param-value> * javax.faces.component.UIInput=x-webkit-speech,x-webkit-grammar; * javax.faces.component.UIComponent=contenteditable,draggable * </param-value> * </context-param> * </pre> * * <h3>Mojarra f:ajax bug</h3> * <p> * Note that <code><f:ajax></code> of Mojarra 2.0.0-2.1.13 explicitly checks for * <code><input type="text"></code> and ignores other types while preparing request parameters for ajax submit, * resulting in <code>null</code> values in managed bean after an ajax submit. This has been reported as * <a href="http://java.net/jira/browse/JAVASERVERFACES-2532">Mojarra issue 2532</a> and is fixed in Mojarra 2.1.14. * This problem is thus completely unrelated to <code>Html5RenderKit</code>. * * <h3>JSF 2.2 notice</h3> * <p> * Noted should be that JSF 2.2 will support defining custom attributes directly in the view via the new * <code>http://xmlns.jcp.org/jsf/passthrough</code> namespace or the <code><f:passThroughAttribute></code> tag. * <pre> * <html ... xmlns:p="http://xmlns.jcp.org/jsf/passthrough"> * ... * <h:inputText ... p:autofocus="true" /> * </pre> * <em>(you may want to use <code>a</code> instead of <code>p</code> as namespace prefix to avoid clash with PrimeFaces * default namespace)</em> * <p> * Or: * <pre> * <h:inputText ...> * <f:passThroughAttribute name="autofocus" value="true" /> * </h:inputText> * </pre> * * <h3>Deprecation</h3> * <p> * As per OmniFaces 2.2 (actually technically already since OmniFaces 2.0), this HTML5 render kit is deprecated. Users * are encouraged to migrate to new JSF 2.2 support for passthrough attributes as described above. The HTML5 render kit * is scheduled to be removed in a future OmniFaces 3.0 (for JSF 2.3). * * @author Bauke Scholtz * @since 1.1 * @deprecated */ @Deprecated public class Html5RenderKit extends RenderKitWrapper { // Constants ------------------------------------------------------------------------------------------------------ /** The context parameter name to specify additional passthrough attributes. */ public static final String PARAM_NAME_PASSTHROUGH_ATTRIBUTES = "org.omnifaces.HTML5_RENDER_KIT_PASSTHROUGH_ATTRIBUTES"; private static final Set<String> HTML5_UIFORM_ATTRIBUTES = unmodifiableSet( "autocomplete" // "novalidate" attribute is not useable in a JSF form. ); private static final Set<String> HTML5_SELECT_ATTRIBUTES = unmodifiableSet( "autofocus" // "form" attribute is not useable in a JSF form. ); private static final Set<String> HTML5_TEXTAREA_ATTRIBUTES = unmodifiableSet( "autofocus", "maxlength", "placeholder", "spellcheck", "wrap" // "form" attribute is not useable in a JSF form. // "required" attribute can't be used as it would override JSF default "required" attribute behaviour. ); private static final Set<String> HTML5_INPUT_ATTRIBUTES = unmodifiableSet( "autofocus", "list", "pattern", "placeholder", "spellcheck" // "form*" attributes are not useable in a JSF form. // "multiple" attribute is only applicable on <input type="email"> and <input type="file"> and can't be // decoded by standard HtmlInputText. // "required" attribute can't be used as it would override JSF default "required" attribute behaviour. ); private static final Set<String> HTML5_INPUT_PASSWORD_ATTRIBUTES = unmodifiableSet( "autofocus", "pattern", "placeholder" // "form*" attributes are not useable in a JSF form. // "required" attribute can't be used as it would override JSF default "required" attribute behaviour. ); private static final Set<String> HTML5_INPUT_RANGE_ATTRIBUTES = unmodifiableSet( "max", "min", "step" ); private static final Set<String> HTML5_INPUT_RANGE_TYPES = unmodifiableSet( "range", "number", "date" ); private static final Set<String> HTML5_INPUT_TYPES = unmodifiableSet( "text", "search", "email", "url", "tel", HTML5_INPUT_RANGE_TYPES ); private static final Set<String> HTML5_BUTTON_ATTRIBUTES = unmodifiableSet( "autofocus" // "form" attribute is not useable in a JSF form. ); private static final String ERROR_INVALID_INIT_PARAM = "Context parameter '" + PARAM_NAME_PASSTHROUGH_ATTRIBUTES + "' is in invalid syntax."; private static final String ERROR_INVALID_INIT_PARAM_CLASS = "Context parameter '" + PARAM_NAME_PASSTHROUGH_ATTRIBUTES + "'" + " references a class which is not found in runtime classpath: '%s'"; private static final String ERROR_UNSUPPORTED_HTML5_INPUT_TYPE = "HtmlInputText type '%s' is not supported. Supported types are " + HTML5_INPUT_TYPES + "."; // Properties ----------------------------------------------------------------------------------------------------- private RenderKit wrapped; private Map<Class<UIComponent>, Set<String>> passthroughAttributes; // Constructors --------------------------------------------------------------------------------------------------- /** * Construct a new HTML5 render kit around the given wrapped render kit. * @param wrapped The wrapped render kit. */ public Html5RenderKit(RenderKit wrapped) { this.wrapped = wrapped; passthroughAttributes = initPassthroughAttributes(); } // Actions -------------------------------------------------------------------------------------------------------- /** * Returns a new HTML5 response writer which in turn wraps the default response writer. */ @Override public ResponseWriter createResponseWriter(Writer writer, String contentTypeList, String characterEncoding) { return new Html5ResponseWriter(super.createResponseWriter(writer, contentTypeList, characterEncoding)); } @Override public RenderKit getWrapped() { return wrapped; } // Helpers -------------------------------------------------------------------------------------------------------- @SuppressWarnings("unchecked") private static Map<Class<UIComponent>, Set<String>> initPassthroughAttributes() { String passthroughAttributesParam = getInitParameter(PARAM_NAME_PASSTHROUGH_ATTRIBUTES); if (isEmpty(passthroughAttributesParam)) { return null; } Map<Class<UIComponent>, Set<String>> passthroughAttributes = new HashMap<>(); for (String passthroughAttribute : passthroughAttributesParam.split("\\s*;\\s*")) { String[] classAndAttributeNames = passthroughAttribute.split("\\s*=\\s*", 2); if (classAndAttributeNames.length != 2) { throw new IllegalArgumentException(ERROR_INVALID_INIT_PARAM); } String className = classAndAttributeNames[0]; Object[] attributeNames = classAndAttributeNames[1].split("\\s*,\\s*"); Set<String> attributeNameSet = unmodifiableSet(attributeNames); try { passthroughAttributes.put((Class<UIComponent>) Class.forName(className), attributeNameSet); } catch (ClassNotFoundException e) { throw new IllegalArgumentException(String.format(ERROR_INVALID_INIT_PARAM_CLASS, className), e); } } return passthroughAttributes; } // Nested classes ------------------------------------------------------------------------------------------------- /** * This HTML5 response writer does all the job. * @author Bauke Scholtz */ class Html5ResponseWriter extends ResponseWriterWrapper { // Properties ------------------------------------------------------------------------------------------------- private ResponseWriter wrapped; // Constructors ----------------------------------------------------------------------------------------------- public Html5ResponseWriter(ResponseWriter wrapped) { this.wrapped = wrapped; } // Actions ---------------------------------------------------------------------------------------------------- @Override public ResponseWriter cloneWithWriter(Writer writer) { return new Html5ResponseWriter(super.cloneWithWriter(writer)); } /** * An override which checks if the given component is an instance of {@link UIForm} or {@link UIInput} and then * write HTML5 attributes which are explicitly been set by the developer. */ @Override public void startElement(String name, UIComponent component) throws IOException { super.startElement(name, component); if (component == null) { return; // Either the renderer is broken, or it's plain text/html. } if (component instanceof UIForm && "form".equals(name)) { writeHtml5AttributesIfNecessary(component.getAttributes(), HTML5_UIFORM_ATTRIBUTES); } else if (component instanceof UIInput) { writeHtml5AttributesIfNecessary((UIInput) component, name); } else if (component instanceof UICommand && "input".equals(name)) { writeHtml5AttributesIfNecessary(component.getAttributes(), HTML5_BUTTON_ATTRIBUTES); } if (passthroughAttributes != null) { for (Entry<Class<UIComponent>, Set<String>> entry : passthroughAttributes.entrySet()) { if (entry.getKey().isInstance(component)) { writeHtml5AttributesIfNecessary(component.getAttributes(), entry.getValue()); } } } } /** * An override which checks if an attribute of <code>type="text"</code> is been written by an {@link UIInput} * component and if so then check if the <code>type</code> attribute isn't been explicitly set by the developer * and if so then write it. * @throws IllegalArgumentException When the <code>type</code> attribute is not supported. */ @Override public void writeAttribute(String name, Object value, String property) throws IOException { if ("type".equals(name) && "text".equals(value)) { UIComponent component = getCurrentComponent(); if (component instanceof HtmlInputText) { Object type = component.getAttributes().get("type"); if (type != null) { if (HTML5_INPUT_TYPES.contains(type)) { super.writeAttribute(name, type, null); return; } else { throw new IllegalArgumentException( String.format(ERROR_UNSUPPORTED_HTML5_INPUT_TYPE, type)); } } } } super.writeAttribute(name, value, property); } @Override public ResponseWriter getWrapped() { return wrapped; } // Helpers ---------------------------------------------------------------------------------------------------- private void writeHtml5AttributesIfNecessary(UIInput component, String name) throws IOException { if (isInput(component, name)) { Map<String, Object> attributes = component.getAttributes(); writeHtml5AttributesIfNecessary(attributes, HTML5_INPUT_ATTRIBUTES); if (HTML5_INPUT_RANGE_TYPES.contains(attributes.get("type"))) { writeHtml5AttributesIfNecessary(attributes, HTML5_INPUT_RANGE_ATTRIBUTES); } } else if (isInputPassword(component, name)) { writeHtml5AttributesIfNecessary(component.getAttributes(), HTML5_INPUT_PASSWORD_ATTRIBUTES); } else if (isTextarea(component, name)) { writeHtml5AttributesIfNecessary(component.getAttributes(), HTML5_TEXTAREA_ATTRIBUTES); } else if (isSelect(component, name)) { writeHtml5AttributesIfNecessary(component.getAttributes(), HTML5_SELECT_ATTRIBUTES); } } private void writeHtml5AttributesIfNecessary(Map<String, Object> attributes, Set<String> names) throws IOException { for (String name : names) { Object value = attributes.get(name); if (value != null) { super.writeAttribute(name, value, null); } } } private boolean isInput(UIInput component, String name) { return component instanceof HtmlInputText && "input".equals(name); } private boolean isInputPassword(UIInput component, String name) { return component instanceof HtmlInputSecret && "input".equals(name); } private boolean isTextarea(UIInput component, String name) { return component instanceof HtmlInputTextarea && "textarea".equals(name); } private boolean isSelect(UIInput component, String name) { return isOneInstanceOf(component.getClass(), UISelectBoolean.class, UISelectOne.class, UISelectMany.class) && ("input".equals(name) || "select".equals(name)); } } }