-
Available since OmniFaces 1.6
The CDI annotation @
Param
allows you to inject, convert and validate a HTTP request or path parameter in a CDI managed bean.
For HTTP request parameters it's basically like <f:viewParam>
, but with the major difference that the injected parameter is directly available during PostConstruct
, allowing a much easier way of processing without the need for a <f:event type="preRenderView">
or <f:viewAction>
in the view.
Usage
Request parameters
The example below injects the request parameter with name foo
.
@Param
private String foo;
By default the name of the request parameter is taken from the name of the variable into which injection takes place. The name can be optionally specified via the name
attribute. The example below injects the request parameter with name foo
into a variable named bar
.
@Param(name="foo")
private String bar;
The name
attribute is only mandatory when using constructor injection in OmniFaces 3.5 or older as there is no information about constructor parameter names. The example below injects the request parameter with name foo
as a constructor parameter.
@Inject
public Bean(@Param(name="foo") String foo) {
// ...
}
Since OmniFaces 3.6 it is not necessary anymore if the parameter has a name according to the class file as per Parameter#isNamePresent()
.
@Inject
public Bean(@Param String foo) {
// ...
}
Multi-valued request parameters
Multi-valued parameters are also supported by specifying a List
or array type. The support was added in OmniFaces 2.4.
@Param(name="foo")
private List<String> foos;
@Param(name="bar")
private String[] bars;
Path parameters
Path parameters can be injected by specifying the pathIndex
attribute representing the zero-based index of the path parameter. The support was added in OmniFaces 2.5. On an example request https://example.com/mypage/firstname.lastname
, which is mapped to /mypage.xhtml
, the below example injects the path parameter firstname.lastname
.
@Param(pathIndex=0)
private String user;
This takes precedence over the name
attribute.
Conversion and validation
Standard types for which Faces already has a build in converter like String
, Long
, Boolean
, etc or for which there's already a converter registered via forClass
, can be injected without explicitly specifying a converter.
@Param
private Long id;
Other types do need a converter. The following is an example of the injection of request parameter user
following a request such as https://example.com/mypage?user=42
:
@Param(converter="userConverter", validator="priviledgedUser")
private User user;
This also works on multi-valued parameters.
@Param(name="user", converter="userConverter")
private List<User> users;
This also works on path parameters. The following is an example of the injection of path parameter user
following a request such as https://example.com/mypage/42
:
@Param(pathIndex=0, converter="userConverter", validator="priviledgedUser")
private User user;
Note that the converter
and validator
attributes can be specified in 3 ways:
- A string value representing the converter/validator ID like so
converter="userConverter"
. - An EL expression returning the converter/validator ID string like so
converter="#{bean.converterId}"
. - An EL expression returning the concrete converter/validator instance like so
converter="#{converterBean}"
.
Instead of converter
or validator
you can also use converterClass
or validatorClass
:
@Param(converterClass=UserConverter.class, validatorClass=PriviledgedUser.class)
private User user;
Note that this is ignored when converter
or validator
is also specified.
In case you want to specify converter or validator attributes, then you can use converterAttributes
or validatorAttributes
respectively. They accept an array of Attribute
arguments whose value can be a string literal or an EL expression such as value = "#{bean.property}"
.
@Param(
converterClass = DateTimeConverter.class,
converterAttributes = { @Attribute(name = "pattern", value = "yyyyMMdd") },
converterMessage = "{1}: \"{0}\" is not the date format we had in mind! Please use the format yyyyMMdd.")
private Date date;
Yes, you can use converterMessage
and validatorMessage
to customize the error message.
In case the converted parameter value is not serializable, while the managed bean is serializable, you could inject it into a field of type ParamValue
, with V
the actual type of the converted parameter. Deserialization in this case works by converting from the original parameter again.
@Inject @Param(converter="someIdToInputStreamConverter")
private ParamValue<InputStream> content; // Extreme example :) Be careful with resource leaking.
If conversion or validation fails, null
is injected if the injection target is NOT ParamValue
. Otherwise a ParamValue
instance is injected, but it will contain a null
value. In both cases, the conversion and validation messages (if any) will be set in the Faces context then, and FacesContext.isValidationFailed()
will return true
.
Faces Messages
By default, faces messages are attached to the client ID of the current UIViewRoot
, so you can use the following for
attribute to separate faces messages coming from @Param
from the rest:
<h:messages for="#{view.id}" />
In case you wish to make it a global message, then you can since OmniFaces 4.5 set the globalMessage
attribute to true
:
@Param(globalMessage=true)
So that these can only be displayed via below tag:
<h:messages globalOnly="true" />
Historical note
Before OmniFaces 3.6, the @
Param
which is not of type ParamValue
also required @
Inject
as in:
@Inject @Param
private String foo;
But this is not needed anymore since OmniFaces 3.6. This has the following advantages:
- Less code
- Not anymore confusing "No bean is eligible for injection to the injection point [JSR-365 §5.2.2]" warnings in IDEs like Eclipse (caused by the dynamic/generic type of the injection point).
DynamicParamValueProducer
. Instead the injection is "manually" done while creating the bean.
CDI issues in EAR
Note that CDI has known issues when the same web fragment library is bundled in multiple WARs in a single EAR and the CDI feature is based on an Extension
.
It's important to understand that those issues are not related to OmniFaces, but to the CDI spec.
For an overview of those issues, please refer Known issues of OmniFaces CDI features in combination with specific application servers.
Needless to say is that EAR is a dead end since introduction of the cloud and has no value in the microservices world.
In other words, if you want to use CDI (or microservices), then you shouldn't be using an EAR in first place.
Click the links below:
The following parameters will be injected and validated as:
- "text1" is injected as
String
and is validated as Facesrequired="true"
- "text2" is injected as
String
and is validated as Faces<f:validateLength minimum="3">
- "text3" is injected as
List<String>
and is validated as JSR303 (Bean Validation)@NotNull
- "number" is injected as
Integer
with automatic Faces integer conversion - "date" is injected as
Date
and is converted as Faces<f:convertDateTime pattern="yyyyMMdd">
- "nsEntity" is injected as
ParamValue<NonSerializableEntity>
and is converted as Faces<f:converter converterId="nonSerializableEntityConverter"/>
(the example also demonstrates support for non-serializable values, which is important for CDI's passivating scopes)
Result: Validation has failed!
- text1: Validation Error: Value is required.
- text3 is required
<p>Click the links below:</p>
<ul>
<li>
<h:link value="Set all params">
<f:param name="text1" value="foo" />
<f:param name="text2" value="bar" />
<f:param name="text3" value="baz1" />
<f:param name="text3" value="baz2" />
<f:param name="number" value="42" />
<f:param name="date" value="19780326" />
<f:param name="nsEntity" value="abc" />
</h:link>
</li>
<li>
<h:link value="Omit required param and set wrong values on others">
<f:param name="text2" value="x" />
<f:param name="number" value="NaN" />
<f:param name="date" value="26 Mar 1978" />
</h:link>
</li>
</ul>
<p>The following parameters will be injected and validated as:</p>
<ul>
<li>"text1" is injected as <code>String</code> and is validated as Faces <code>required="true"</code></li>
<li>"text2" is injected as <code>String</code> and is validated as
Faces <code><f:validateLength minimum="3">
</code></li>
<li>"text3" is injected as <code>List<String></code> and is validated as JSR303 (Bean Validation) <code>@NotNull</code></li>
<li>"number" is injected as <code>Integer</code> with automatic Faces integer conversion</li>
<li>"date" is injected as <code>Date</code> and is converted as
Faces <code><f:convertDateTime pattern="yyyyMMdd"></code>
</li>
<li>"nsEntity" is injected as <code>ParamValue<NonSerializableEntity></code> and is
converted as Faces <code><f:converter converterId="nonSerializableEntityConverter"/></code>
<small>(the example also demonstrates support for non-serializable values, which is important for CDI's passivating scopes)</small>
</li>
</ul>
<p>Result: #{cdiParamBean.result}</p>
<h:messages styleClass="messages" infoClass="info" errorClass="error" />
package org.omnifaces.showcase.cdi;
import static org.omnifaces.util.Faces.isValidationFailed;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.List;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.FacesException;
import jakarta.faces.convert.DateTimeConverter;
import jakarta.faces.validator.LengthValidator;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull;
import org.omnifaces.cdi.Param;
import org.omnifaces.cdi.param.Attribute;
import org.omnifaces.cdi.param.ParamValue;
import org.omnifaces.showcase.model.NonSerializableEntity;
import org.omnifaces.util.Messages;
@Named
@RequestScoped
public class CdiParamBean {
// Like <f:viewParam name="text1" value="#{bean.text1}" required="true">
@Inject @Param(required = true)
private String text1;
// Like <f:viewParam name="text2" value="#{bean.text2}" validatorMessage="..."><f:validateLength minimum="3">
@Inject @Param(
validatorClasses = LengthValidator.class,
validatorAttributes = @Attribute(name="minimum", value="3"),
validatorMessage = "{1}: Value is too too small! Please enter a minimum of 3 characters.")
private String text2;
// Multi-valued parameters are not possible with <f:viewParam>; using JSR303 bean validation via the @NotNull constraint.
@Inject @Param @NotNull(message="{0} is required")
private List<String> text3;
// Like <f:viewParam name="number" value="#{bean.number}"> using implicit Faces integer converter.
@Inject @Param
private Integer number;
// Like <f:viewParam name="date" value="#{bean.date}" converterMessage="..."><f:convertDateTime pattern="yyyyMMdd">
@Inject @Param(
converterClass = DateTimeConverter.class,
converterAttributes = { @Attribute(name="pattern", value="yyyyMMdd") },
converterMessage="{1}: \"{0}\" is not the date format we had in mind! Please use the format yyyyMMdd.")
private Date date;
// Like <f:viewParam name="nsEntity" value="#{bean.nsEntity}" converter="nonSerializableEntityConverter">
@Inject @Param(converter = "nonSerializableEntityConverter")
private ParamValue<NonSerializableEntity> nsEntity;
private String result;
@PostConstruct
public void init() {
if (isValidationFailed()) {
result = "Validation has failed!";
return;
}
// Simulate serialization.
ParamValue<NonSerializableEntity> nsEntityCopy = copy(nsEntity);
result = String.format("You entered text1 '%s', text2 '%s', text3 '%s', number '%d', date '%5$tY%5$tm%5$td',"
+ " entity '%6$s' and entity copy '%7$s'", text1, text2, text3, number, date, nsEntity.getValue(),
nsEntityCopy.getValue());
Messages.addGlobalInfo("Yes, no validation errors!");
}
public String getResult() {
return result;
}
@SuppressWarnings("unchecked")
public static <T> T copy(T source) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
T copy = null;
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(source);
}
catch (IOException e) {
throw new FacesException(e);
}
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
copy = (T) ois.readObject();
}
catch (IOException | ClassNotFoundException e) {
throw new FacesException(e);
}
return copy;
}
}