javax.faces.component.UIWebsocket Maven / Gradle / Ivy
Show all versions of javax.faces Show documentation
package javax.faces.component;
import static java.util.Collections.unmodifiableList;
import static javax.faces.push.PushContext.ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.regex.Pattern;
import javax.el.ValueExpression;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.push.Push;
import javax.faces.push.PushContext;
import javax.websocket.CloseReason.CloseCodes;
/**
*
* The <f:websocket>
tag opens an one-way (server to client) websocket based push connection in client
* side which can be reached from server side via {@link PushContext} interface injected in any CDI/container managed
* artifact via @
{@link Push} annotation.
*
*
*
* By default, the rendererType
property must be set to "javax.faces.Websocket
".
* This value can be changed by calling the setRendererType()
method.
*
*
*
* For detailed usage instructions, see @
{@link Push} javadoc.
*
*
* @see Push
* @since 2.3
*/
public class UIWebsocket extends UIComponentBase implements ClientBehaviorHolder {
// ---------------------------------------------------------------------------------------------- Manifest Constants
/**
*
* The standard component type for this component.
*
*/
public static final String COMPONENT_TYPE = "javax.faces.Websocket";
/**
*
* The standard component family for this component.
*
*/
public static final String COMPONENT_FAMILY = "javax.faces.Script";
/**
*
* Properties that are tracked by state saving.
*
*/
enum PropertyKeys {
channel, scope, user, onopen, onmessage, onclose, connected;
}
private static final Pattern PATTERN_CHANNEL_NAME = Pattern.compile("[\\w.-]+");
private static final String ERROR_ENDPOINT_NOT_ENABLED =
"f:websocket endpoint is not enabled."
+ " You need to set web.xml context param '" + ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME + "' with value 'true'.";
private static final String ERROR_INVALID_CHANNEL =
"f:websocket 'channel' attribute '%s' does not represent a valid channel name. It is required, it may not be an"
+ " EL expression and it may only contain alphanumeric characters, hyphens, underscores and periods.";
private static final String ERROR_INVALID_USER =
"f:websocket 'user' attribute '%s' does not represent a valid user identifier. It must implement Serializable and"
+ " preferably have low memory footprint. Suggestion: use #{request.remoteUser} or #{someLoggedInUser.id}.";
// ---------------------------------------------------------------------------------------------------- Constructors
/**
*
* Create a new {@link UIWebsocket} instance with default property values.
*
*
* @throws IllegalStateException When Websocket endpoint is not enabled.
*/
public UIWebsocket() {
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
if (!Boolean.parseBoolean(externalContext.getInitParameter(ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME))) {
throw new IllegalStateException(ERROR_ENDPOINT_NOT_ENABLED);
}
}
// --------------------------------------------------------------------------------------------- UIComponent Methods
/**
*
* Returns {@link UIWebsocket#COMPONENT_FAMILY}.
*
*/
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
/**
*
* Set the {@link ValueExpression} used to calculate the value for the specified attribute or property name, if any.
* If a {@link ValueExpression} is set for the channel
or scope
property, regardless of
* the value, throw an illegal argument exception. If a {@link ValueExpression} is set for the user
* property, and the non-null value is not an instance of Serializable
, throw an illegal argument
* exception.
*
*
* @throws IllegalArgumentException If name
is one of id
, parent
,
* channel
or scope
, or it name
is user
and the non-null value
* is not an instance of Serializable
.
* @throws NullPointerException If name
is null
.
*/
@Override
public void setValueExpression(String name, ValueExpression binding) {
if (PropertyKeys.channel.toString().equals(name) || PropertyKeys.scope.toString().equals(name)) {
throw new IllegalArgumentException(name);
}
if (PropertyKeys.user.toString().equals(name)) {
Object user = binding.getValue(getFacesContext().getELContext());
if (user != null && !(user instanceof Serializable)) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_USER, user));
}
}
super.setValueExpression(name, binding);
}
// ------------------------------------------------------------------------------------ ClientBehaviorHolder Methods
/**
*
* Returns a non-null, empty, unmodifiable Collection
which returns true
on any
* Collection#contains()
invocation, indicating that all client behavior event names are acceptable.
*
*/
@Override
public Collection getEventNames() {
return CONTAINS_EVERYTHING;
}
private static final Collection CONTAINS_EVERYTHING = unmodifiableList(new ArrayList() {
private static final long serialVersionUID = 1L;
@Override
public boolean contains(Object object) {
return true;
}
});
// ------------------------------------------------------------------------------------------------------ Properties
/**
* Returns the name of the websocket channel.
* @return The name of the websocket channel.
*/
public String getChannel() {
return (String) getStateHelper().eval(PropertyKeys.channel);
}
/**
* Sets the name of the websocket channel.
* It may not be an EL expression and it may only contain alphanumeric characters, hyphens, underscores and periods.
* All open websockets on the same channel will receive the same push message from the server.
* @param channel The name of the websocket channel.
* @throws IllegalArgumentException When the value does not represent a valid channel name.
*/
public void setChannel(String channel) {
if (channel == null || !PATTERN_CHANNEL_NAME.matcher(channel).matches()) {
throw new IllegalArgumentException(String.format(ERROR_INVALID_CHANNEL, channel));
}
getStateHelper().put(PropertyKeys.channel, channel);
}
/**
* Returns the scope of the websocket channel.
* @return The scope of the websocket channel.
*/
public String getScope() {
return (String) getStateHelper().eval(PropertyKeys.scope);
}
/**
* Sets the scope of the websocket channel.
* It may not be an EL expression and allowed values are application
, session
and
* view
, case insensitive. When the value is application
, then all channels with the same
* name throughout the application will receive the same push message. When the value is session
, then
* only the channels with the same name in the current user session will receive the same push message. When the
* value is view
, then only the channel in the current view will receive the push message. The default
* scope is application
. When the user
attribute is specified, then the default scope is
* session
.
* @param scope The scope of the websocket channel.
*/
public void setScope(String scope) {
getStateHelper().put(PropertyKeys.scope, scope);
}
/**
* Returns the user identifier of the websocket channel.
* @return The user identifier of the websocket channel.
*/
public Serializable getUser() {
return (Serializable) getStateHelper().eval(PropertyKeys.user);
}
/**
* Sets the user identifier of the websocket channel, so that user-targeted push messages can be sent.
* All open websockets on the same channel and user will receive the same push message from the server.
* It must implement Serializable
and preferably have low memory footprint.
* Suggestion: use #{request.remoteUser}
or #{someLoggedInUser.id}
.
* @param user The user identifier of the websocket channel.
*/
public void setUser(Serializable user) {
getStateHelper().put(PropertyKeys.user, user);
}
/**
* Returns the JavaScript event handler function that is invoked when the websocket is opened.
* @return The JavaScript event handler function that is invoked when the websocket is opened.
*/
public String getOnopen() {
return (String) getStateHelper().eval(PropertyKeys.onopen);
}
/**
* Sets the JavaScript event handler function that is invoked when the websocket is opened.
* The function will be invoked with one argument: the channel name.
* @param onopen The JavaScript event handler function that is invoked when the websocket is opened.
*/
public void setOnopen(String onopen) {
getStateHelper().put(PropertyKeys.onopen, onopen);
}
/**
* Returns the JavaScript event handler function that is invoked when a push message is received from the server.
* @return The JavaScript event handler function that is invoked when a push message is received from the server.
*/
public String getOnmessage() {
return (String) getStateHelper().eval(PropertyKeys.onmessage);
}
/**
* Sets the JavaScript event handler function that is invoked when a push message is received from the server. The
* function will be invoked with three arguments: the push message, the channel name and the raw MessageEvent itself.
* @param onmessage The JavaScript event handler function that is invoked when a push message is received from the server.
*/
public void setOnmessage(String onmessage) {
getStateHelper().put(PropertyKeys.onmessage, onmessage);
}
/**
* Returns the JavaScript event handler function that is invoked when the websocket is closed.
* @return The JavaScript event handler function that is invoked when the websocket is closed.
*/
public String getOnclose() {
return (String) getStateHelper().eval(PropertyKeys.onclose);
}
/**
* Sets the JavaScript event handler function that is invoked when the websocket is closed.
* The function will be invoked with three arguments: the close reason code, the channel name and the raw
* CloseEvent
itself. Note that this will also be invoked on errors and that you can inspect the close
* reason code if an error occurred and which one (i.e. when the code is not 1000). See also
* RFC 6455 section 7.4.1 and {@link CloseCodes} API
* for an elaborate list of all close codes.
* @param onclose The JavaScript event handler function that is invoked when the websocket is closed.
*/
public void setOnclose(String onclose) {
getStateHelper().put(PropertyKeys.onclose, onclose);
}
/**
* Returns whether to (auto)connect the websocket or not.
* @return Whether to (auto)connect the websocket or not.
*/
public boolean isConnected() {
return (Boolean) getStateHelper().eval(PropertyKeys.connected, Boolean.TRUE);
}
/**
* Sets whether to (auto)connect the websocket or not. Defaults to true
. It's interpreted as a
* JavaScript instruction whether to open or close the websocket push connection. Note that this attribute is
* re-evaluated on every ajax request. You can also explicitly set it to false
and then manually
* control in JavaScript by OmniFaces.Push.open("channelName")
and
* OmniFaces.Push.close("channelName")
.
* @param connected Whether to (auto)connect the websocket or not.
*/
public void setConnected(boolean connected) {
getStateHelper().put(PropertyKeys.connected, connected);
}
}