org.omnifaces.renderkit.Html5RenderKit Maven / Gradle / Ivy
/*
* Copyright 2012 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;
/**
*
* 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 autocomplete
* 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:
*
* - {@link UIForm}:
autocomplete
* - {@link UISelectBoolean}, {@link UISelectOne} and {@link UISelectMany}:
autofocus
* - {@link HtmlInputText}:
type
(supported values are text
(default), search
, email
, url
, tel
, range
, number
and date
)autofocus
list
pattern
placeholder
spellcheck
min
max
step
(the latter three are only supported on type
of range
, number
and date
)
* - {@link HtmlInputTextarea}:
autofocus
maxlength
placeholder
spellcheck
wrap
* - {@link HtmlInputSecret}:
autofocus
pattern
placeholder
* - {@link HtmlCommandButton}:
autofocus
*
*
* Note: the list
attribute expects a <datalist>
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 HTML5 tutorial.
*
*
Installation
*
* To use the HTML5 render kit, register it as follows in faces-config.xml
:
*
* <factory>
* <render-kit-factory>org.omnifaces.renderkit.Html5RenderKitFactory</render-kit-factory>
* </factory>
*
*
* Configuration
*
* You can also configure additional passthrough attributes via the
* {@value org.omnifaces.renderkit.Html5RenderKit#PARAM_NAME_PASSTHROUGH_ATTRIBUTES} context parameter in
* web.xml
, wherein the passthrough attributes are been specified in semicolon-separated
* com.example.SomeComponent=attr1,attr2,attr3
key=value pairs. The key represents the fully qualified
* name of a class whose {@link Class#isInstance(Object)} must return true
for the particular component
* and the value represents the commaseparated string of names of passthrough attributes. Here's an example:
*
* <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>
*
*
* Mojarra f:ajax bug
*
* Note that <f:ajax>
of Mojarra 2.0.0-2.1.13 explicitly checks for
* <input type="text">
and ignores other types while preparing request parameters for ajax submit,
* resulting in null
values in managed bean after an ajax submit. This has been reported as
* Mojarra issue 2532 and is fixed in Mojarra 2.1.14.
* This problem is thus completely unrelated to Html5RenderKit
.
*
*
JSF 2.2 notice
*
* Noted should be that JSF 2.2 will support defining custom attributes directly in the view via the new
* http://xmlns.jcp.org/jsf/passthrough
namespace or the <f:passThroughAttribute>
tag.
*
* <html ... xmlns:p="http://xmlns.jcp.org/jsf/passthrough">
* ...
* <h:inputText ... p:autofocus="true" />
*
* (you may want to use a
instead of p
as namespace prefix to avoid clash with PrimeFaces
* default namespace)
*
* Or:
*
* <h:inputText ...>
* <f:passThroughAttribute name="autofocus" value="true" />
* </h:inputText>
*
*
* @author Bauke Scholtz
* @since 1.1
*/
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 HTML5_UIFORM_ATTRIBUTES = unmodifiableSet(
"autocomplete"
// "novalidate" attribute is not useable in a JSF form.
);
private static final Set HTML5_SELECT_ATTRIBUTES = unmodifiableSet(
"autofocus"
// "form" attribute is not useable in a JSF form.
);
private static final Set 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 HTML5_INPUT_ATTRIBUTES = unmodifiableSet(
"autofocus", "list", "pattern", "placeholder", "spellcheck"
// "form*" attributes are not useable in a JSF form.
// "multiple" attribute is only applicable on and 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 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 HTML5_INPUT_RANGE_ATTRIBUTES = unmodifiableSet(
"max", "min", "step"
);
private static final Set HTML5_INPUT_RANGE_TYPES = unmodifiableSet(
"range", "number", "date"
);
private static final Set HTML5_INPUT_TYPES = unmodifiableSet(
"text", "search", "email", "url", "tel", HTML5_INPUT_RANGE_TYPES
);
private static final Set 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, Set> 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, Set> initPassthroughAttributes() {
String passthroughAttributesParam = getInitParameter(PARAM_NAME_PASSTHROUGH_ATTRIBUTES);
if (isEmpty(passthroughAttributesParam)) {
return null;
}
Map, Set> 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 attributeNameSet = unmodifiableSet(attributeNames);
try {
passthroughAttributes.put((Class) 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, Set> entry : passthroughAttributes.entrySet()) {
if (entry.getKey().isInstance(component)) {
writeHtml5AttributesIfNecessary(component.getAttributes(), entry.getValue());
}
}
}
}
/**
* An override which checks if an attribute of type="text"
is been written by an {@link UIInput}
* component and if so then check if the type
attribute isn't been explicitly set by the developer
* and if so then write it.
* @throws IllegalArgumentException When the type
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 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 attributes, Set 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));
}
}
}