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;
/**
* <p class="changed_added_2_3">
* The <code><f:websocket></code> 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 <code>@</code>{@link Push} annotation.
* </p>
*
* <p>
* By default, the <code>rendererType</code> property must be set to "<code>javax.faces.Websocket</code>".
* This value can be changed by calling the <code>setRendererType()</code> method.
* </p>
*
* <p>
* For detailed usage instructions, see <code>@</code>{@link Push} javadoc.
* </p>
*
* @see Push
* @since 2.3
*/
public class UIWebsocket extends UIComponentBase implements ClientBehaviorHolder {
// ---------------------------------------------------------------------------------------------- Manifest Constants
/**
* <p>
* The standard component type for this component.
* </p>
*/
public static final String COMPONENT_TYPE = "javax.faces.Websocket";
/**
* <p>
* The standard component family for this component.
* </p>
*/
public static final String COMPONENT_FAMILY = "javax.faces.Script";
/**
* <p>
* Properties that are tracked by state saving.
* </p>
*/
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
/**
* <p>
* Create a new {@link UIWebsocket} instance with default property values.
* </p>
*
* @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
/**
* <p>
* Returns {@link UIWebsocket#COMPONENT_FAMILY}.
* </p>
*/
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
/**
* <p>
* 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 <code>channel</code> or <code>scope</code> property, regardless of
* the value, throw an illegal argument exception. If a {@link ValueExpression} is set for the <code>user</code>
* property, and the non-null value is not an instance of <code>Serializable</code>, throw an illegal argument
* exception.
* </p>
*
* @throws IllegalArgumentException If <code>name</code> is one of <code>id</code>, <code>parent</code>,
* <code>channel</code> or <code>scope</code>, or it <code>name</code> is <code>user</code> and the non-null value
* is not an instance of <code>Serializable</code>.
* @throws NullPointerException If <code>name</code> is <code>null</code>.
*/
@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
/**
* <p>
* Returns a non-null, empty, unmodifiable <code>Collection</code> which returns <code>true</code> on any
* <code>Collection#contains()</code> invocation, indicating that all client behavior event names are acceptable.
* </p>
*/
@Override
public Collection<String> getEventNames() {
return CONTAINS_EVERYTHING;
}
private static final Collection<String> CONTAINS_EVERYTHING = unmodifiableList(new ArrayList<String>() {
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 <code>application</code>, <code>session</code> and
* <code>view</code>, case insensitive. When the value is <code>application</code>, then all channels with the same
* name throughout the application will receive the same push message. When the value is <code>session</code>, then
* only the channels with the same name in the current user session will receive the same push message. When the
* value is <code>view</code>, then only the channel in the current view will receive the push message. The default
* scope is <code>application</code>. When the <code>user</code> attribute is specified, then the default scope is
* <code>session</code>.
* @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 <code>Serializable</code> and preferably have low memory footprint.
* Suggestion: use <code>#{request.remoteUser}</code> or <code>#{someLoggedInUser.id}</code>.
* @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
* <code>CloseEvent</code> 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
* <a href="http://tools.ietf.org/html/rfc6455#section-7.4.1">RFC 6455 section 7.4.1</a> 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 <code>true</code>. 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 <code>false</code> and then manually
* control in JavaScript by <code>OmniFaces.Push.open("channelName")</code> and
* <code>OmniFaces.Push.close("channelName")</code>.
* @param connected Whether to (auto)connect the websocket or not.
*/
public void setConnected(boolean connected) {
getStateHelper().put(PropertyKeys.connected, connected);
}
}