jakarta.faces.component.UIWebsocket Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.faces.component;
import static jakarta.faces.push.PushContext.ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME;
import static java.util.Collections.unmodifiableList;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.regex.Pattern;
import jakarta.el.ValueExpression;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.push.Push;
import jakarta.faces.push.PushContext;
import jakarta.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 "jakarta.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 = "jakarta.faces.Websocket";
/**
*
* The standard component family for this component.
*
*/
public static final String COMPONENT_FAMILY = "jakarta.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"
+ "Jakarta Expression Language 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 Jakarta Expression Language 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 Jakarta Expression Language 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);
}
}