Soklet Logo

Core Concepts

Servlet Integration

Soklet is not a Servlet Container - it has its own in-process HTTP server, its own approach to request and response constructs, and so forth. Soklet applications are intended to be "vanilla" Java applications, as opposed to a WAR file deployed onto a Java EE App Server.

However, there is a large body of existing code that relies on the Servlet API. To support it, Soklet provides its own implementations of the following Servlet interfaces, which enable interoperability for many common use cases:

Support is available for both legacy javax.servlet and current jakarta.servlet specifications. Just like Soklet, these integrations have zero dependencies (not counting Soklet itself and the javax.servlet-api/jakarta.servlet-api spec JARs) - drop the appropriate JAR into your project and you're good to go.


Installation

Maven

If you use javax.servlet:

<dependency>
  <groupId>com.soklet</groupId>
  <artifactId>soklet-servlet-javax</artifactId>
  <version>1.0.0</version>
</dependency>

If you use jakarta.servlet:

<dependency>
  <groupId>com.soklet</groupId>
  <artifactId>soklet-servlet-jakarta</artifactId>
  <version>1.0.0</version>
</dependency>

Gradle

If you use javax.servlet:

repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.soklet:soklet-servlet-javax:1.0.0'
}

If you use jakarta.servlet:

repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.soklet:soklet-servlet-jakarta:1.0.0'
}

Usage

A normal Servlet API integration looks like the following:

  1. Given a Soklet Request, create both an HttpServletRequest and an HttpServletResponse.
  2. Write whatever is needed to HttpServletResponse
  3. Convert the HttpServletResponse to a Soklet MarshaledResponse
@GET("/servlet-example")
public MarshaledResponse servletExample(Request request) {
  // Create an HttpServletRequest from the Soklet Request
  HttpServletRequest httpServletRequest = 
    SokletHttpServletRequest.withRequest(request).build();

  // Create an HttpServletResponse from the Soklet Request
  SokletHttpServletResponse httpServletResponse = 
    SokletHttpServletResponse.withRequest(request);

  // Write some data to the response using Servlet APIs
  Cookie cookie = new Cookie("name", "value");
  cookie.setDomain("soklet.com");
  cookie.setMaxAge(60);
  cookie.setPath("/");

  httpServletResponse.setStatus(200);
  httpServletResponse.addHeader("test", "one");
  httpServletResponse.addHeader("test", "two");
  httpServletResponse.addCookie(cookie);
  httpServletResponse.setCharacterEncoding("ISO-8859-1");
  httpServletResponse.getWriter().print("test");    
  
  // Convert HttpServletResponse into a Soklet MarshaledResponse and return it
  return httpServletResponse.toMarshaledResponse();
}

Instantiation

Soklet's Servlet API types are final and prefer static factory methods to constructors.

Here's how to acquire an instance of each.

HttpServletRequest

Standard approach, uses defaults:

@GET("/servlet-example")
public MarshaledResponse servletExample(Request request) {
  HttpServletRequest httpServletRequest = 
    SokletHttpServletRequest.withRequest(request).build();
  
  // ... rest of method elided ...
}

Custom approach:

@GET("/servlet-example")
public MarshaledResponse servletExample(Request request) {
  // Get references to some dependencies...
  ServletContext servletContext = SokletServletContext.withDefaults();
  HttpSession httpSession = SokletHttpSession.withServletContext(servletContext);

  // ...and supply to the builder:
  HttpServletRequest httpServletRequest =
    SokletHttpServletRequest.withRequest(request)
      .port(1234)
      .host("0.0.0.0")
      .servletContext(servletContext)
      .httpSession(httpSession)
      .build();
  
  // ... rest of method elided ...
}

HttpServletResponse

Standard approach:

@GET("/servlet-example")
public MarshaledResponse servletExample(Request request) {
  HttpServletResponse response = 
    SokletHttpServletResponse.withRequest(request);
  // ... rest of method elided ...
}

Explicit request path:

// Request path must always starts with "/"
HttpServletResponse response = 
  SokletHttpServletResponse.withRequestPath("/test/abc");

HttpSession

// Sessions need a context...
ServletContext servletContext = SokletServletContext.withDefaults();
// ...so we get one and wire it in:
HttpSession httpSession = 
  SokletHttpSession.withServletContext(servletContext);

HttpSessionContext

Deprecation Warning

The Servlet spec has marked javax.servlet.http.HttpSessionContext as deprecated since Servlet API 2.1 with no replacement, so it's unlikely you will need to use this type - but it's available regardless.

This type is not available for jakarta.servlet integration.

HttpSessionContext httpSessionContext = SokletHttpSessionContext.withDefaults();

ServletContext

Standard approach:

ServletContext servletContext = SokletServletContext.withDefaults();

Explicit java.io.Writer which acts as a sink for ServletContext::log(String) and ServletContext::log(String, Throwable) invocations:

try(FileWriter logWriter = new FileWriter("example.txt")) {
  ServletContext servletContext = 
    SokletServletContext.withLogWriter(logWriter);

  // Fire off a log event to be consumed by the writer
  servletContext.log("Just a test");
}

ServletInputStream

try(InputStream inputStream = ...) {
  ServletInputStream servletInputStream = 
    SokletServletInputStream.withInputStream(inputStream);

  // TODO: do some work with servletInputStream
}

ServletOutputStream

Standard approach:

try(OutputStream outputStream = ...) {
  ServletOutputStream servletOutputStream = 
    SokletServletOutputStream.withOutputStream(outputStream);
  
  // TODO: do some work with servletOutputStream
}

With listeners:

try(OutputStream outputStream = ...) {
  ServletOutputStream servletOutputStream = 
    SokletServletOutputStream.withOutputStream(outputStream)
      .onWriteOccurred((servletOutputStream, writtenByte) -> {
        // Take action when a write occurred
      })
      .onWriteFinalized((servletOutputStream) -> {
        // Take action when the writes have been finalized (totally done)
      })    
      .build();
  
  // TODO: do some work with servletOutputStream
}

ServletPrintWriter

Standard approach:

try(Writer writer = ...) {
  ServletPrintWriter servletPrintWriter = 
    SokletServletPrintWriter.withWriter(writer).build();
  
  // TODO: do some work with writer
}

With listeners:

try(Writer writer = ...) {
  ServletPrintWriter servletPrintWriter = 
    SokletServletPrintWriter.withWriter(writer)
      .onWriteOccurred((servletPrintWriter, writerEvent) -> {
        // Take action when a write occurred
      })
      .onWriteFinalized((servletPrintWriter) -> {
        // Take action when the writes have been finalized (totally done)
      })    
      .build();
  
  // TODO: do some work with servletPrintWriter
}

Real-World Example

Suppose you'd like to have your application function as a SAML Service Provider (SP), where you redirect to a SAML Identity Provider (IdP) to perform authentication and rely on it to POST back a cryptographically-signed assertion confirming the user is who she says she is.

You might prefer to use a library like OneLogin's SAML Java Toolkit instead of writing your own SP implementation. Because this library is designed specifically for the Servlet API, Soklet's Servlet integration is necessary to use it.

Here's how an integration might look.

First, we need to be able to redirect to the IdP for login:

@GET("/saml/redirect-to-login")
public MarshaledResponse redirectToSamlLogin(Request request) {
  // Create an HttpServletRequest from the Soklet Request
  HttpServletRequest httpServletRequest = 
    SokletHttpServletRequest.withRequest(request).build();

  // Create an HttpServletResponse from the Soklet Request
  SokletHttpServletResponse httpServletResponse = 
    SokletHttpServletResponse.withRequest(request);

  // Configure SAML Java Toolkit (details not shown)...
  Saml2Settings settings = acquireSaml2Settings();

  // ...and let it do its work on the HttpServletRequest/HttpServletResponse.
  // Behind the scenes, SAML Java Toolkit generates a cryptographically-
  // signed URL and writes headers to perform an HTTP 302 redirect to it
  Auth auth = new Auth(settings, httpServletRequest, httpServletResponse);
  auth.login("https://my.example.api/saml/process-assertion");

  // Convert HttpServletResponse into a MarshaledResponse and return it.
  // Any headers/cookies/body/etc. written to the HttpServletResponse
  // are carried over into the Soklet marshaled-response representation
  return httpServletResponse.toMarshaledResponse();
}

Next, we need a way to process the XML assertion sent back to us by the IdP after a successful login:

@POST("/saml/process-assertion")
public Response processSamlAssertion(
  Request request,
  MyExampleBackend myExampleBackend
) {
  // Create an HttpServletRequest from the Soklet Request
  HttpServletRequest httpServletRequest = 
    SokletHttpServletRequest.withRequest(request).build();

  // Create an HttpServletResponse from the Soklet Request
  SokletHttpServletResponse httpServletResponse = 
    SokletHttpServletResponse.withRequest(request);

  // Configure SAML Java Toolkit (details not shown)...
  Saml2Settings settings = acquireSaml2Settings();

  // ...and let it do its work.
  // Here, SAML Java Toolkit parses the assertion and ensures that
  // the IdP's private key was used to sign it.
  Auth auth = new Auth(settings, httpServletRequest, httpServletResponse);
  auth.processResponse();

  if (auth.isAuthenticated()) {
    // Everything looks good...
    // 1. Pull the user identifier out of the assertion
    // 2. Use it to generate a JWT
    // 3. Write the JWT to a cookie
    // 4. Redirect the user somewhere meaningful
    String jwt = myExampleBackend.jwtForNameId(auth.getNameId());

    return Response.withRedirect(
        RedirectType.HTTP_307_TEMPORARY_REDIRECT,
        "https://my.example.website/home"
      ).cookies(Set.of(
        ResponseCookie.with("Access-Token", jwt).build()
      ))
      .build();      
  } else {
    // Real systems should handle this more gracefully
    throw new RuntimeException("The SAML assertion is invalid");
  }
}
Previous
Testing