Showing posts with label web. Show all posts
Showing posts with label web. Show all posts

Saturday, February 13, 2021

HTML over the wire

Yes please!

DHH is on to something with his recent post on HTML over the wire. It's high time that we start tearing down this gigantic mount of complexity that we've built the last two decades and get back to application development that developers can actually understand and excell with!

Tuesday, January 8, 2019

Building a WebSocket application with Spring Boot

This article shows you how to setup a very basic WebSocket application powered by Spring Boot. It uses the Java API for WebSocket (JSR-356) to define a WebSocket server endpoint and includes a very simple JavaScript WebSocket client.

As usual for a Spring Boot application, it all starts with the Maven POM:

  <?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

      <modelVersion>4.0.0</modelVersion>
      <groupId>com.ervacon.sb</groupId>
      <artifactId>websocket</artifactId>
      <version>0.0.1-SNAPSHOT</version>

      <!-- Inherit defaults from Spring Boot -->
      <parent>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.1.1.RELEASE</version>
      </parent>

      <!-- Add typical dependencies for a websocket application -->
      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-websocket</artifactId>
          </dependency>
      </dependencies>

      <!-- Package as an executable jar -->
      <build>
          <plugins>
              <plugin>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
              </plugin>
          </plugins>
      </build>

  </project>
Notice how we pull in the spring-boot-starter-websocket dependency. This will make sure the Java API for WebSocket (javax.websocket) is on the classpath, together with Spring's support for WebSocket. When Spring Boot starts it will detect these dependencies and auto-configure the embedded Servlet engine (Tomcat by default) to handle the WebSocket protocol.

Next, we define our @SpringBootApplication class:

package com.ervacon.sb.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
The only special thing here is the fact that we register the ServerEndpointExporter, which will make sure our @ServerEndpoint annotated classes get registered with the WebSocket runtime.

We can now define our server side WebSocket endpoint:

package com.ervacon.sb.websocket;

import org.springframework.stereotype.Component;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@Component
@ServerEndpoint("/socket")
public class WebSocketEndpoint {

    @OnMessage
    public String echo(String message) {
        return "Echo: " + message;
    }
}
Nothing to surprising here: we're just using the Java API for WebSocket (JSR-356) to define a method that will handle incoming text messages by simply echoing them back to the client. That client is implemented using some basic JavaScript code in a simple HTML page (add this as index.html to src/main/resources/static in your Spring Boot application):
<html>
  <head>
      <title>WebSocket Client</title>
      <!-- based on https://www.nexmo.com/blog/2018/10/08/create-websocket-server-spring-boot-dr/ -->
  </head>
  <body>
  <div class="container">
      <div id="messages" style="border: 1px solid gray; padding: 1em;"></div>
      <div class="input-fields">
          <p>Type your message:</p>
          <input id="message"/>
          <button id="send">Send</button>
      </div>
  </div>
  </body>
  <script>
      const messageWindow = document.getElementById("messages");

      const sendButton = document.getElementById("send");
      const messageInput = document.getElementById("message");

      const socket = new WebSocket("ws://localhost:8080/socket");

      socket.onopen = function (event) {
          addMessageToWindow("Connected");
      };

      socket.onmessage = function (event) {
          addMessageToWindow(`Got Message: ${event.data}`);
      };

      sendButton.onclick = function (event) {
          sendMessage(messageInput.value);
          messageInput.value = "";
      };

      function sendMessage(message) {
          socket.send(message);
          addMessageToWindow("Sent Message: " + message);
      }

      function addMessageToWindow(message) {
          messageWindow.innerHTML += `<div>${message}</div>`
      }
  </script>
</html>
We can now launch our Spring Boot application using a simple mvn spring-boot:run and test it by accessing http://localhost:8080/.

Tuesday, January 8, 2013

Check caching of HTTP resources

Setting up HTTP caching can be a bit of pain. Essentially the HTTP response needs to contain appropriate cache control headers, either an Expires header or a Cache-Control max-age directive. Here's a quick Java program using the HttpClient and HttpClient Cache (both part of the Apache HttpComponents) to test client-side caching of HTTP responses.
public class TestHttpCaching {

 public static void main(String[] args) throws Exception {
  CacheConfig config = new CacheConfig();
  config.setMaxObjectSize(Long.MAX_VALUE);
  HttpClient httpClient = new CachingHttpClient(config);

  doGetRequest(httpClient, args[0]);
  doGetRequest(httpClient, args[0]);
 }

 private static void doGetRequest(HttpClient httpClient, String url) throws Exception {
  HttpGet httpGet = new HttpGet(url);
  HttpContext httpContext = new BasicHttpContext();
  try {
   System.out.print("Getting... ");
   System.out.print(httpClient.execute(httpGet, httpContext).getStatusLine());
   System.out.println(": " + httpContext.getAttribute(CachingHttpClient.CACHE_RESPONSE_STATUS));
  } finally {
   httpGet.releaseConnection();
  }
 }
}
The standard DefaultHttpClient does not do any caching. Instead you have to use the CachingHttpClient which wraps a DefaultHttpClient and adds caching functionality. Also notice how the cache is configured with a very large maximum object size. This avoids resources not being cached because they are too large for the standard in-memory cache (BasicHttpCacheStorage).

Friday, January 1, 2010

REST Reservations

I'm currently reading RESTful Web Services by Leonard Richardson and Sam Ruby, and happened to come across an interesting QCon talk by Mark Nottingham on the status of HTTP. To me, this talk carries an interesting critical undertone on REST.

I've followed the whole REST and RESTful Web Services hype with some interest the last few years, and genuinely like several aspects of REST:
  • You can't go wrong with a KISS approach to software development.
  • Most everybody is familiar with how the Web works, so exploiting that familiarity makes a lot of sense.
  • The HTTP underpinnings of REST make it very well supported by a multitude of tools.
  • By leveraging HTTP, REST has excellent support for intermediaries (i.e. proxies and caches) and all the cool things they can bring to the table.

On the other hand, I also have some reservations about the whole REST thing, and Mark Nottingham touched on several of these topics in his talk:
  • As always, there is a lot of dogma to go around, and everybody has his own flavour. Is using this or that HTTP method in a particular way RESTful? Mark, who's obviously intimately familiar with HTTP, seems to have a much more pragmatic view on how you can or should use HTTP (i.e. listen to his comments on POST at the very end of the talk).
  • HTTP is deceptively simple. The spec is BIG, and full of ambiguities. The entire first part of Mark's talk deals with this, and I would argue that most people (me included) only have a superficial familiarity with HTTP.
  • Another thing I dislike about REST is that it's not generally applicable. For instance, suppose you're developing a search service, like Google (a typical text-book RESTful Web Service example). Most everybody in the REST community agrees that you should use HTTP GET for the search and encode the query in the URI: http://www.google.be/search?q=jellyfish. Imagine a similar search service that allows you to search for images that look like a given image. Logically this is completely in-line with the simple search service, but because of technical limitations of HTTP GET you're forced to implement this in a different way (WS-* is more consistently applicable this way).

For me, it's all about sane software engineering. If you have a well designed service, using established standards and conventions where relevant, people are going to be able to use your service without much problems. Things like REST and WS-* all have there place as tools in a developers toolbox, and it's up to us developers to make informed decisions on when to use what to build the best possible software we can.

Tuesday, November 24, 2009

Page Flow Challenge Clarification

The Page Flow Challenge in my previous post warrants a bit of clarification. A few readers had questions about the usefulness or sanity of certain steps in the test scenario. I'll go through the scenario and highlight what is being tested in each of the steps.
  1. Start the page flow
  2. On Page 1, enter "foo" and click Next
    You should end up on Page 2 where "foo" is displayed as input from page 1
  3. On Page 2, enter "baz" and click Next
    You should end up on Page 3 where "foo" and "baz" are displayed as input from the previous pages
  4. Click the browser back button
    Without any browser warnings you should end up on Page 2, where "foo" is still displayed as input from page 1
The first part of the scenario sets up a basic set of data associated with the page flow: "foo" and "baz" at this point. At step 4 we're using the browser back button to backtrack. This tests POST-REDIRECT-GET behaviour: if the application does not use POST-REDIRECT-GET, the browser will show a warning before displaying Page 2 again. This is so because Page 2 is the result of posting the HTML form on Page 1, and modern browsers never automatically resubmit post data. Essentially we're protecting the user from accidental resubmits. Not having the browser show ugly warnings about resubmitting also makes the application feel stable and trustworthy.
  1. Right click the Back link on the page and do Open Link in New Window
    The new window should show Page 1 where "foo" is displayed in the input field
  2. In the new window, enter "kit" (replacing "foo") and click Next
    You should end up on Page 2 where "kit" is displayed as input from page 1
  3. In the new window, on Page 2 enter "kat" and click Next
    You should end up on Page 3 where "kit" and "kat" are displayed as input from the previous pages
  4. In the original window, click the browser refresh button
    Without any browser warnings you should see Page 2 again with "foo" as input from page 1
Step 5, admittedly a contrived step, simulates the user branching off the page flow into two separate windows. The point of doing this is testing whether or not the two branches are isolated. In steps 6 and 7 new data is entered ("kit" and "kat") that potentially interferes with the previously entered data ("foo" and "baz"). Step 8 verifies that no interference happened: the page flow in the original window still has "foo" as data.
  1. In the original window, enter "bar" and click Next
    You should end up on Page 3 with "foo" and "bar" as input from previous pages
  2. In the original window, click Finish
    You should end up on the start page of the application: the page flow has finished
  3. In the original window, click the browser back button
    You should either receive an error immediately, or when you try to resubmit by clicking Finish again
In step 9 and 10, we complete the page flow and finally submit values "foo" and "bar". At this point the page flow has finished. The state and navigation management of the framework should have invalidated the page flow at this point, making it impossible to resubmit. Step 11 verifies this: if you go back using the browser back button, you should not be allowed to resubmit. This part of the scenario tests intentional double submit protection (classically solved with something like a synchronizer token).
  1. (For extra bonus points) In the new window, (where you still are on Page 3) click Finish
    You should receive an error of some sort (potentially a automatic page flow restart)
The final step of the scenario tests whether or not the framework tracks the two branches of the page flow as being part of the same original conversation. This is an advanced form of double submit protection. In most cases this is a beneficial extra protection. In some cases this might not be what you want.

To make all of this a bit more tangible, let's suppose this is an airline ticket booking flow. The scenario would go something like this:
In step 2, on Page 1, the users fills in basic traveller information (name, age, seating class) and advances forward. In step 3, on Page 2, he selects a seating preference. Page 3 in step 4 shows an overview of the captured information and asks for confirmation. The user notices that he made a mistake and backs up.
Steps 5, 6 and 7 has the user thinking he might want to try a seat in a different seating class and compare the prices. Page 3 in step 7 displays an overview of the price with the new seating class.
In step 8 and 9 the user completes the page flow with the original seating class. On step 10 he compares the prices of the two alternatives and decides to submit the first of the two alternatives.
Steps 11 and 12 verify that the user cannot accidentally book two tickets, either by resubmitting the first alternative or submitting the second alternative. If he really wants another ticket, he'll have to start the ticket booking process (the page flow) again.

I hope this clarifies things a bit and shows that the test scenario is actually somewhat realistic.

Sunday, November 22, 2009

A Page Flow Challenge

In this post I setup a page flow challenge to find out if the new or up-and-coming web frameworks handle page flows well.

Why am I doing this? Over the last several years I've seen quite a few blogs and discussions where people talk about how web framework X handles page flows, typically comparing the page flow functionality in framework X with Spring Web Flow. The problem with most of these comparisons is that they focus on just two parts of the page flow puzzle: expressing page flows through some kind of DSL, and reusing page flows as modules throughout the application. There is an important third piece: managing navigation and the associated state. The reason why this is often overlooked is probably because the navigation and state management is completely automatic in Spring Web Flow, so many people don't even realize that it's there.

While attending a session on the Lift framework last week at Devoxx09, this issue came up again. Somebody (not me :) asked whether "Lift has page flow functionality like Spring Web Flow?". The answer was as expected: the Lift developers have a page flow DSL in the works but seem to have missed the navigation and state management part (update: this appears to be incorrect, Lift does have navigation and state management.).

It's not enough to be able to define a page flow, the framework also needs to manage it! To help users and framework developers realize this, I've setup a page flow challenge. The idea is that you develop a simple 3-page page flow in your framework of choice and test whether it can correctly run a somewhat contrived scenario that tests things like POST-REDIRECT-GET to avoid accidental resubmission, double submit protection to avoid intentional resubmission, and correct state management. In essence the page flow should behave correctly whatever the user does and protect the user from potential mistakes.

Let's look at the page flow:



Easy peasy. This could for instance be a flow to submit a payment in an electronic banking application, or a flow to handle checkout in a web shop.

The page flow challenge scenario is as follows (note: I've clarified this scenario in a follow-up post):
  1. Start the page flow
  2. On Page 1, enter "foo" and click Next
    You should end up on Page 2 where "foo" is displayed as input from page 1
  3. On Page 2, enter "baz" and click Next
    You should end up on Page 3 where "foo" and "baz" are displayed as input from the previous pages
  4. Click the browser back button
    Without any browser warnings you should end up on Page 2, where "foo" is still displayed as input from page 1
  5. Right click the Back link on the page and do Open Link in New Window
    The new window should show Page 1 where "foo" is displayed in the input field
  6. In the new window, enter "kit" (replacing "foo") and click Next
    You should end up on Page 2 where "kit" is displayed as input from page 1
  7. In the new window, on Page 2 enter "kat" and click Next
    You should end up on Page 3 where "kit" and "kat" are displayed as input from the previous pages
  8. In the original window, click the browser refresh button
    Without any browser warnings you should see Page 2 again with "foo" as input from page 1
  9. In the original window, enter "bar" and click Next
    You should end up on Page 3 with "foo" and "bar" as input from previous pages
  10. In the original window, click Finish
    You should end up on the start page of the application: the page flow has finished
  11. In the original window, click the browser back button
    You should either receive an error immediately, or when you try to resubmit by clicking Finish again
  12. (For extra bonus points) In the new window, (where you still are on Page 3) click Finish
    You should receive an error of some sort (potentially a automatic page flow restart)
UPDATE: I've introduced a new step 11 in the above scenario, and what used to be step 11 is now step 12.

The only extra rule to keep in mind is that the HTML forms should use POST, to follow proper web design rules.

I've made reference implementations of the page flow challenge in Spring Web Flow 1 and 2. The source code is available in Subversion:

Spring Web Flow 1: https://svn.ervacon.com/public/spring/samples/trunk/pageflowchallenge-swf1/
Spring Web Flow 2: https://svn.ervacon.com/public/spring/samples/trunk/pageflowchallenge-swf2/

If you have Subversion and Maven installed, getting things up and running is as simple as:
$ svn export https://svn.ervacon.com/public/spring/samples/trunk/pageflowchallenge-swf2/ pageflowchallenge-swf2
$ cd pageflowchallenge-swf2
$ mvn jetty:run
You can now point your browser to http://localhost:8080/pageflowchallenge-swf2/ to test things out.

This challenge is solely focussed on page flow functionality, so it's not meant to reflect on the quality of a particular framework in other areas. For instance, SWF1 and SWF2 handle this scenario flawlessly, but probably have a harder time dealing with some other use-cases.

If you implemented the challenge in your web framework of choice, please let me know in the comments! I'm especially interested to see how Wicket, Seam, Lift and GWT fare. If you think this is irrelevant for your web framework because it handles things differently, I'd love to learn why you think this is the case, and how a use-case like this would then be implemented.