5

I have several WebSockets endpoints such as,

wss://localhost:8181/ContextPath/Push

All of such endpoint URLs are hard-coded in separate, external JavaScript files (.js). These JavaScript files are included in respective XHTML files as and when required. The host name and the context path should be evaluated programmatically instead of hard-coding all over the place where they are required.

The host name (localhost:8181) can be obtained in JavaScript using document.location.host but there is no standard/canonical way in JavaScript to obtain a context path where the application runs.


I am doing something like the following.

A global JavaScript variable is declared on the master template as follows.

<f:view locale="#{bean.locale}" encoding="UTF-8" contentType="text/html">
        <f:loadBundle basename="messages.ResourceBundle" var="messages"/>

        <ui:param name="contextPath" value="#{request.contextPath}"/>
        <ui:insert name="metaData"></ui:insert>

        <h:head>
            <script type="text/javascript">var contextPath = "#{contextPath}";</script>
        </h:head>

        <h:body id="body">

        </h:body>
    </f:view>
</html>

The JavaScript files in which the host name and the context path are hard-coded are included in respective template clients or any of sections of the template north, south, east and west as follows.

<html lang="#{bean.language}"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:form>
        <h:outputScript library="default" name="js/websockets.js" target="head"/>
    </h:form>

For the sake of viewpoint only, websockets.js looks like the following (you can simply ignore it).

if (window.WebSocket) {
    // The global variable "contextPath" is unavailable here
    // because it is declared afterwards in the generated HTML.
    var ws = new WebSocket("wss://"+document.location.host + contextPath + "/Push");
    ws.onmessage = function (event) {
        // This handler is invoked, when a message is received through a WebSockets channel.
    };

    $(window).on('beforeunload', function () {
        ws.close();
    });
} else {}

Now, the global JavaScript variable contextPath declared in the master template is expected to be available in the included JavaScript file namely websockets.js. This is however untrue.

What happens is that the included JavaScript file namely websockets.js where the global variable contextPath is attempted to be accessed, is placed before the hard-coded <script> tag in the generated HTML <head> tag in the master template.

In other words, the global JavaScript variable contextPath is actually attempted to use in the included file websockets.js before being declared.

Anyway, how to get rid of hard-coding the context path in external JavaScript files?

The sole purpose of doing this is that unlike CSS files, EL isn't evaluated in external JavaScript files. Therefore, #{} thing will not work unless it is placed in an XHTML file.

Aritz
  • 30,971
  • 16
  • 136
  • 217
Tiny
  • 27,221
  • 105
  • 339
  • 599

2 Answers2

8

What happens is that the included JavaScript file named websockets.js where the global variable contextPath is attempted to be accessed, is placed before the hard-coded <script> tag in the generated HTML <head> tag in the master template

This is unexpected. You declared the <h:outputScript> referring websockets.js file inside <h:body> with target="head". This is supposed to end up after all other script resources already declared in <h:head>. See also a.o. How to reference CSS / JS / image resource in Facelets template? After all, this appears to be caused by PrimeFaces bundled HeadRenderer which is intented to auto-include some CSS resources and take care of the <facet name="first|middle|last">.

This is worth an issue report to PF guys (if not already done). In the meanwhile, your best bet is to turn off it by explicitly registering the JSF implementation's own HeadRenderer back as below in faces-config.xml (provided that you're using Mojarra).

<render-kit>
    <renderer>
        <component-family>javax.faces.Output</component-family>
        <renderer-type>javax.faces.Head</renderer-type>
        <renderer-class>com.sun.faces.renderkit.html_basic.HeadRenderer</renderer-class>
    </renderer>
</render-kit>

And explicitly include the PrimeFaces theme-specific theme.css as below in <h:head>:

<h:outputStylesheet library="primefaces-aristo" name="theme.css" />

Coming back to the real question,

Anyway, how to get rid of hard-coding the context path in external JavaScript files?

Either set it as base URI (note: relative path isn't supported in HTML4 / IE6-8).

<h:head>
    <base href="#{request.contextPath}/" />
    ...
</h:head>
var baseURI = $("base").attr("href");

Or set it as data attribute of HTML root element.

<!DOCTYPE html>
<html lang="en" data-baseuri="#{request.contextPath}/" ...>
    ...
</html>
var baseURI = $("html").data("baseuri");

Unrelated to the concrete problem, as a word of advice, to transparently cover both http+ws and https+wss, consider using location.protocol instead of a hardcoded wss.

var ws = new WebSocket(location.protocol.replace("http", "ws") + "//" + location.host + baseURI + "Push");
Community
  • 1
  • 1
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • I picked up the last approach using `data-baseuri`. Is there any caveat using this approach though I ignore old/buggy/obscure browsers? In the meanwhile, I came across [this](http://blog.primefaces.org/?p=1433) link. Since PrimeFaces 3.0, it is possible to define a `` with `name="first"`, `name="middle"` and `name="last"` which allows resources to be placed in the said order in the generated HTML `` element. (Is the `replace()` function here is a special function to replace protocol names? It does not appear to be a simple character replacement function). – Tiny Sep 02 '15 at 18:25
  • jQuery worries about obscure browsers. Coming back to head ordering, I just realized that whatever you observed is not the expected behavior. Which JSF impl/version are you using? I just tried once again on Mojarra 2.2.12 and the script declared in the form just ended up in the end of the head, after the script setting the global variable (note that you can also just use ``). As to `String#replace()` in JS, it's basically the same as `String#replaceAll()` in Java, indeed regex based. But it should end up in `http` being replaced by `ws` and `https` by `wss` (keeping the `s`). – BalusC Sep 02 '15 at 18:39
  • I use the same version, Mojarra 2.2.12 along with PrimeFaces 5.2 (community release). – Tiny Sep 02 '15 at 19:09
  • Why does it say, `Unable to create a new instance of 'com.sun.faces.ren??derkit.html_basic.HeadRenderer': javax.faces.FacesException: com.sun.faces.ren??derkit.html_basic.HeadRenderer`, when attempting to register `HeadRenderer`? – Tiny Nov 12 '15 at 17:59
  • There are no question marks in the `faces-config.xml` file. They are merely shown in the exception stack-trace starting with `java.lang.Exception: java.lang.IllegalStateException: ContainerBase.addChild: start: org.apache.catalina.LifecycleException: java.lang.RuntimeException: com.sun.faces.config.ConfigurationException: Source Document: jndi:/server/Project-war/WEB-INF/faces-config.xml`. The `HeadRenderer` class is available in the said package. – Tiny Nov 12 '15 at 18:11
  • Weird. An invisible illegal character? Try retyping the value. – BalusC Nov 12 '15 at 18:32
  • Retyping fixed the problem. Thanks. – Tiny Nov 12 '15 at 18:46
2

Is the following an option for you?

  1. Define a hidden html-tag in the master template, something like :

    <span id="pageContextPath" data="#{contextPath}" style="display:none;"></span>
    
  2. Change your JavaScript Code to something like :

    jQuery(document).ready(function ($) {
        if (window.WebSocket) {
            contextPath = $("#pageContextPath").attr("data");
            var ws = new WebSocket("wss://" + document.location.host + contextPath + "/Push");
            //...
        } else {}
    });
    

I used here jQuery. You may rewrite it in plain JavaScript. But it should be done after "document ready" to ensure that the hidden tag has been rendered. otherwise js won't find that element.

Tiny
  • 27,221
  • 105
  • 339
  • 599
Rami.Q
  • 2,486
  • 2
  • 19
  • 30