Added documentation to migrate from Servlet to Handler.

Introduced CompletableTask to simplify Content.Source reads.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-08-11 19:10:55 +02:00
parent b126407c4e
commit 8f6a38aca8
4 changed files with 777 additions and 19 deletions

View File

@ -97,8 +97,52 @@
[[pg-migration-11-to-12-servlet-to-handler]]
==== Migrate Servlets to Jetty Handlers
Web applications written using the Servlet APIs may be re-written using the Jetty `Handler` APIs.
The sections below outline the Jetty `Handler` APIs that correspond to the Servlet APIs.
To replace the functionalities of Servlet Filters, refer to xref:pg-server-http-handler[this section].
===== Handler Request APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=request]
----
===== Handler Request Content APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-string]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-buffer]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-stream]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-source]
----
===== Handler Response APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=response]
----
===== Handler Response Content APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit-status]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-explicit]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-content]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-string]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-echo]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-trailers]
----
[[pg-migration-11-to-12-api-changes]]
==== APIs Changes

View File

@ -0,0 +1,643 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.docs.programming.migration;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CompletableTask;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Promise;
import static java.nio.charset.StandardCharsets.UTF_8;
@SuppressWarnings("unused")
public class ServletToHandlerDocs
{
@SuppressWarnings("InnerClassMayBeStatic")
// tag::request[]
public class RequestAPIs extends Handler.Abstract
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Gets the request method.
// Replaces:
// - servletRequest.getMethod();
String method = request.getMethod();
// Gets the request protocol name and version.
// Replaces:
// - servletRequest.getProtocol();
String protocol = request.getConnectionMetaData().getProtocol();
// Gets the full request URI.
// Replaces:
// - servletRequest.getRequestURL();
String fullRequestURI = request.getHttpURI().asString();
// Gets the request context.
// Replaces:
// - servletRequest.getServletContext()
Context context = request.getContext();
// Gets the context path.
// Replaces:
// - servletRequest.getContextPath()
String contextPath = context.getContextPath();
// Gets the request path.
// Replaces:
// - servletRequest.getRequestURI();
String requestPath = request.getHttpURI().getPath();
// Gets the request path after the context path.
// Replaces:
// - servletRequest.getServletPath() + servletRequest.getPathInfo()
String pathInContext = Request.getPathInContext(request);
// Gets the request query.
// Replaces:
// - servletRequest.getQueryString()
String queryString = request.getHttpURI().getQuery();
// Gets request parameters.
// Replaces:
// - servletRequest.getParameterNames();
// - servletRequest.getParameter(name);
// - servletRequest.getParameterValues(name);
// - servletRequest.getParameterMap();
Fields queryParameters = Request.extractQueryParameters(request, UTF_8);
Fields allParameters = Request.getParameters(request);
// Gets cookies.
// Replaces:
// - servletRequest.getCookies();
List<HttpCookie> cookies = Request.getCookies(request);
// Gets request HTTP headers.
// Replaces:
// - servletRequest.getHeaderNames()
// - servletRequest.getHeader(name)
// - servletRequest.getHeaders(name)
// - servletRequest.getDateHeader(name)
// - servletRequest.getIntHeader(name)
HttpFields requestHeaders = request.getHeaders();
// Gets the request Content-Type.
// Replaces:
// - servletRequest.getContentType()
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
// Gets the request Content-Length.
// Replaces:
// - servletRequest.getContentLength()
// - servletRequest.getContentLengthLong()
long contentLength = request.getHeaders().getLongField(HttpHeader.CONTENT_LENGTH);
// Gets the request locales.
// Replaces:
// - servletRequest.getLocale()
// - servletRequest.getLocales()
List<Locale> locales = Request.getLocales(request);
// Gets the request scheme.
// Replaces:
// - servletRequest.getScheme()
String scheme = request.getHttpURI().getScheme();
// Gets the server name.
// Replaces:
// - servletRequest.getServerName()
String serverName = Request.getServerName(request);
// Gets the server port.
// Replaces:
// - servletRequest.getServerPort()
int serverPort = Request.getServerPort(request);
// Gets the remote host/address.
// Replaces:
// - servletRequest.getRemoteAddr()
// - servletRequest.getRemoteHost()
String remoteAddress = Request.getRemoteAddr(request);
// Gets the remote port.
// Replaces:
// - servletRequest.getRemotePort()
int remotePort = Request.getRemotePort(request);
// Gets the local host/address.
// Replaces:
// - servletRequest.getLocalAddr()
// - servletRequest.getLocalHost()
String localAddress = Request.getLocalAddr(request);
// Gets the local port.
// Replaces:
// - servletRequest.getLocalPort()
int localPort = Request.getLocalPort(request);
// Gets the request attributes.
// Replaces:
// - servletRequest.getAttributeNames()
// - servletRequest.getAttribute(name)
// - servletRequest.setAttribute(name, value)
// - servletRequest.removeAttribute(name)
String name = "name";
Object value = "value";
Set<String> names = request.getAttributeNameSet();
Object attribute = request.getAttribute(name);
request.setAttribute(name, value);
request.removeAttribute(name);
request.clearAttributes();
Map<String, Object> map = request.asAttributeMap();
// Gets the request trailers.
// Replaces:
// - servletRequest.getTrailerFields()
HttpFields trailers = request.getTrailers();
// Gets the HTTP session.
// Replaces:
// - servletRequest.getSession()
// - servletRequest.getSession(create)
boolean create = true;
Session session = request.getSession(create);
callback.succeeded();
return false;
}
}
// end::request[]
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsString extends Handler.Abstract
{
// tag::requestContent-string[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Non-blocking read the request content as a String.
// Use with caution as the request content may be large.
Promise.Completable<String> completable = new Promise.Completable<>();
Content.Source.asString(request, UTF_8, completable);
completable.whenComplete((requestContent, failure) ->
{
if (failure == null)
{
// Process the request content here.
// Implicitly respond with status code 200 and no content.
callback.succeeded();
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::requestContent-string[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsByteBuffer extends Handler.Abstract
{
// tag::requestContent-buffer[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Non-blocking read the request content as a ByteBuffer.
// Use with caution as the request content may be large.
Promise.Completable<ByteBuffer> completable = new Promise.Completable<>();
Content.Source.asByteBuffer(request, completable);
completable.whenComplete((requestContent, failure) ->
{
if (failure == null)
{
// Process the request content here.
// Implicitly respond with status code 200 and no content.
callback.succeeded();
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::requestContent-buffer[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsInputStream extends Handler.Abstract
{
// tag::requestContent-stream[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Read the request content as an InputStream.
// Note that InputStream.read() may block.
try (InputStream inputStream = Content.Source.asInputStream(request))
{
while (true)
{
int read = inputStream.read();
// EOF was reached, stop reading.
if (read < 0)
break;
// Process the read byte here.
}
}
// Implicitly respond with status code 200 and no content.
callback.succeeded();
return true;
}
// end::requestContent-stream[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsSource extends Handler.Abstract
{
// tag::requestContent-source[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
CompletableTask<Void> reader = new CompletableTask<>()
{
@Override
public void run()
{
// Read in a loop.
while (true)
{
// Read a chunk of content.
Content.Chunk chunk = request.read();
// If there is no content, demand to be
// called back when more content is available.
if (chunk == null)
{
request.demand(this);
return;
}
// If a failure is read, complete with a failure.
if (Content.Chunk.isFailure(chunk))
{
Throwable failure = chunk.getFailure();
completeExceptionally(failure);
return;
}
if (chunk instanceof Trailers trailers)
{
// Possibly process the request trailers here.
// Trailers have an empty ByteBuffer and are a last chunk.
}
// Process the request content chunk here.
// After the processing, the chunk MUST be released.
chunk.release();
// If the last chunk is read, complete normally.
if (chunk.isLast())
{
complete(null);
return;
}
// Not the last chunk of content, loop around to read more.
}
}
};
// Initiate the read of the request content.
reader.start();
// When the read is complete, complete the Handler callback.
callback.completeWith(reader);
return true;
}
// end::requestContent-source[]
}
@SuppressWarnings("InnerClassMayBeStatic")
// tag::response[]
public class ResponseAPIs extends Handler.Abstract
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Sets/Gets the response HTTP status.
// Replaces:
// - servletResponse.setStatus(code);
// - servletResponse.getStatus();
response.setStatus(HttpStatus.OK_200);
int status = response.getStatus();
// Gets the response HTTP headers.
// Replaces:
// - servletResponse.setHeader(name, value);
// - servletResponse.addHeader(name, value);
// - servletResponse.setDateHeader(name, date);
// - servletResponse.addDateHeader(name, date);
// - servletResponse.setIntHeader(name, value);
// - servletResponse.addIntHeader(name, value);
// - servletResponse.getHeaderNames()
// - servletResponse.getHeader(name)
// - servletResponse.getHeaders(name)
// - servletResponse.containsHeader(name)
HttpFields.Mutable responseHeaders = response.getHeaders();
// Sets an HTTP cookie.
// Replaces:
// - servletResponse.addCookie(cookie);
HttpCookie cookie = HttpCookie.build("name", "value")
.domain("example.org")
.path("/path")
.maxAge(Duration.ofDays(1).toSeconds())
.build();
Response.addCookie(response, cookie);
// Sets the response Content-Type.
// Replaces:
// - servletResponse.setContentType(type)
responseHeaders.put(HttpHeader.CONTENT_TYPE, "text/plain; charset=UTF-8");
// Sets the response Content-Length.
// Replaces:
// - servletResponse.setContentLength(length)
// - servletResponse.setContentLengthLong(length)
responseHeaders.put(HttpHeader.CONTENT_LENGTH, 1024L);
// Sets/Gets the response trailers.
// Replaces:
// - servletResponse.setTrailerFields(() -> trailers)
// - servletResponse.getTrailerFields()
HttpFields trailers = HttpFields.build().put("checksum", 0xCAFE);
response.setTrailersSupplier(trailers);
Supplier<HttpFields> trailersSupplier = response.getTrailersSupplier();
// Gets whether the response is committed.
// Replaces:
// - servletResponse.isCommitted()
boolean committed = response.isCommitted();
// Resets the response.
// Replaces:
// - servletResponse.reset();
response.reset();
// Sends a redirect response.
// Replaces:
// - servletResponse.encodeRedirectURL(location)
// - servletResponse.sendRedirect(location)
String location = Request.toRedirectURI(request, "/redirect");
Response.sendRedirect(request, response, callback, location);
// Sends an error response.
// Replaces:
// - servletResponse.sendError(code);
// - servletResponse.sendError(code, message);
Response.writeError(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, "Request Cannot be Processed");
callback.succeeded();
return true;
}
}
// end::response[]
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIsImplicit extends Handler.Abstract
{
// tag::responseContent-implicit[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Produces an implicit response with status code 200
// with no content when returning from this method.
// The Handler callback must be completed when returning true.
callback.succeeded();
return true;
}
// end::responseContent-implicit[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIsImplicitWithStatus extends Handler.Abstract
{
// tag::responseContent-implicit-status[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Produces an implicit response with status 204
// with no content when returning from this method.
response.setStatus(HttpStatus.NO_CONTENT_204);
// The Handler callback must be completed when returning true.
callback.succeeded();
return true;
}
// end::responseContent-implicit-status[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIsExplicit extends Handler.Abstract
{
// tag::responseContent-explicit[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Produces an explicit response with status 204 with no content.
response.setStatus(HttpStatus.NO_CONTENT_204);
// This explicit first write() writes the response status code and headers.
// It is also the last write (as specified by the first parameter)
// and writes an empty content (the second parameter, a null ByteBuffer).
// When this write completes, the Handler callback is completed.
response.write(true, null, callback);
return true;
}
// end::responseContent-explicit[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPISimpleContent extends Handler.Abstract
{
// tag::responseContent-content[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
ByteBuffer content = UTF_8.encode("Hello World");
// Explicit first write that writes the response status code, headers and content.
// When this write completes, the Handler callback is completed.
response.write(true, content, callback);
return true;
}
// end::responseContent-content[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIFlush extends Handler.Abstract
{
// tag::responseContent-content[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
ByteBuffer content = UTF_8.encode("Hello World");
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, content.remaining());
// Flush the response status code and the headers (no content).
// This is the fist but non-last write.
Callback.Completable completable = new Callback.Completable();
response.write(false, null, completable);
// When the first write completes, perform the second (and last) write.
completable.whenComplete((ignored, failure) ->
{
if (failure == null)
{
// Now explicitly write the content as the last write.
// When this write completes, the Handler callback is completed.
response.write(true, content, callback);
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::responseContent-content[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIString extends Handler.Abstract
{
// tag::responseContent-string[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
// Utility method to write UTF-8 string content.
// When this write completes, the Handler callback is completed.
Content.Sink.write(response, true, "Hello World", callback);
return true;
}
// end::responseContent-string[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIEcho extends Handler.Abstract
{
// tag::responseContent-echo[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
// Utility method to echo the content from the request to the response.
// When the echo completes, the Handler callback is completed.
Content.copy(request, response, callback);
return true;
}
// end::responseContent-echo[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPITrailers extends Handler.Abstract
{
// tag::responseContent-trailers[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
// The trailers must be set on the response before the first write.
HttpFields.Mutable trailers = HttpFields.build();
response.setTrailersSupplier(trailers);
// Explicit first write that writes the response status code, headers and content.
// The trailers have not been written yet; they will be written with the last write.
ByteBuffer content = UTF_8.encode("Hello World");
Callback.Completable completable = new Callback.Completable();
response.write(false, content, completable);
completable.whenComplete((ignored, failure) ->
{
if (failure == null)
{
// Update the trailers
trailers.put("Content-Checksum", 0xCAFE);
// Explicit last write to write the trailers
// and complete the Handler callback.
response.write(true, null, callback);
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::responseContent-trailers[]
}
}

View File

@ -42,6 +42,7 @@ import org.eclipse.jetty.io.content.PathContentSource;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CompletableTask;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.IO;
@ -104,7 +105,9 @@ public class ContentSourceTest
return List.of(asyncSource, byteBufferSource, transformerSource, pathSource, inputSource, inputSource2);
}
/** Get the next chunk, blocking if necessary
/**
* Get the next chunk, blocking if necessary
*
* @param source The source to get the next chunk from
* @return A non null chunk
*/
@ -113,8 +116,7 @@ public class ContentSourceTest
Content.Chunk chunk = source.read();
if (chunk != null)
return chunk;
FuturePromise<Content.Chunk> next = new FuturePromise<>();
Runnable getNext = new Runnable()
CompletableTask<Content.Chunk> task = new CompletableTask<>()
{
@Override
public void run()
@ -122,18 +124,12 @@ public class ContentSourceTest
Content.Chunk chunk = source.read();
if (chunk == null)
source.demand(this);
next.succeeded(chunk);
else
complete(chunk);
}
};
source.demand(getNext);
try
{
return next.get();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
source.demand(task);
return task.join();
}
@ParameterizedTest
@ -141,8 +137,7 @@ public class ContentSourceTest
public void testRead(Content.Source source) throws Exception
{
StringBuilder builder = new StringBuilder();
CountDownLatch eof = new CountDownLatch(1);
source.demand(new Runnable()
var task = new CompletableTask<>()
{
@Override
public void run()
@ -162,14 +157,14 @@ public class ContentSourceTest
if (chunk.isLast())
{
eof.countDown();
complete(null);
break;
}
}
}
});
assertTrue(eof.await(10, TimeUnit.SECONDS));
};
source.demand(task);
task.get(10, TimeUnit.SECONDS);
assertThat(builder.toString(), is("onetwo"));
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util;
import java.util.concurrent.CompletableFuture;
/**
* <p>A {@link CompletableFuture} that implements {@link Runnable} to perform
* a one-shot task that eventually completes this {@link CompletableFuture}.</p>
* <p>Subclasses override {@link #run()} to implement the task.</p>
* <p>Users of this class start the task execution via {@link #start()}.</p>
* <p>Typical usage:</p>
* <pre>{@code
* CompletableTask<T> task = new CompletableTask<>()
* {
* @Override
* public void run()
* {
* try
* {
* // Perform some task.
* T result = performTask();
*
* // Eventually complete this CompletableFuture.
* complete(result);
* }
* catch (Throwable x)
* {
* completeExceptionally(x);
* }
* }
* }
*
* // Start the task and then process the
* // result of the task when it is complete.
* task.start()
* .whenComplete((result, failure) ->
* {
* if (failure == null)
* {
* // The task completed successfully.
* }
* else
* {
* // The task failed.
* }
* });
* }</pre>
*
* @param <T> the type of the result of the task
*/
public abstract class CompletableTask<T> extends CompletableFuture<T> implements Runnable
{
/**
* <p>Starts the task by calling {@link #run()}
* and returns this {@link CompletableTask}.</p>
*
* @return this {@link CompletableTask}
*/
public CompletableTask<T> start()
{
run();
return this;
}
}