Repository: jooby-project/jooby
Pull request #1944 (2020-08-25 22:45:25)

UnitOfWork port from 1.x
See #1920


Patch:
diff --git a/docs/asciidoc/modules/hibernate.adoc b/docs/asciidoc/modules/hibernate.adoc
index 876eea95d..29e49236b 100644
--- a/docs/asciidoc/modules/hibernate.adoc
+++ b/docs/asciidoc/modules/hibernate.adoc
@@ -188,6 +188,106 @@ Otherwise, you are going to see `LazyInitializationException`.
 There is a javadoc:hibernate.SessionRequest[] decorator that works identically but leaves transaction
 management to you, so no transaction is started/committed or rollback during a HTTP request.
 
+=== UnitOfWork
+
+Another way of managing the lifecycle of an EntityManager and transactions is working with an instance
+of `UnitOfWork`. You may acquire one from the service registry or inject it via DI.
+
+Usage:
+
+.UnitOfWork
+[source, java, role = "primary"]
+----
+{
+  get("/pets", ctx -> require(UnitOfWork.class)
+      .apply(em -> em.createQuery("from Pet", Pet.class).getResultList()));
+}
+----
+
+.Kotlin
+[source, kt, role="secondary"]
+----
+{
+  get("/pets") {
+    require(UnitOfWork::class.java)
+        .apply { em ->
+          em.createQuery("from Pet", Pet::class.java).resultList
+        }
+  }
+}
+----
+
+`UnitOfWork` automatically begins a transaction. After the code block passed to `apply(...)` or `accept(...)`
+returns the transaction is being committed and the `EntityManager` closed.
+
+If the code block throws an exception, the transaction is rolled back, and the `EntityManager` is released as well.
+You may access a `UnitOfWork.TransactionHandler` instance to be able to work with multiple transactions:
+
+.Multiple transactions
+[source, java, role = "primary"]
+----
+{
+  get("/update", ctx -> require(UnitOfWork.class)
+      .apply((em, txh) -> {
+        em.createQuery("from Pet", Pet.class).getResultList().forEach(pet -> {
+          pet.setName(pet.getName() + " Updated");
+          txh.commit(); // update each entity in a separate transaction
+        });
+        return "ok";
+      }));
+}
+----
+
+.Kotlin
+[source, kt, role="secondary"]
+----
+{
+  get("/update") {
+    require(UnitOfWork::class.java)
+        .apply { em, txh ->
+          em.createQuery("from Pet", Pet::class.java).resultList.forEach { pet ->
+            pet.name = "${pet.name} Updated"
+            txh.commit() // update each entity in a separate transaction
+          }
+          "ok"
+        }
+  }
+}
+----
+
+A call to `UnitOfWork.TransactionHandler.commit()` commits the current transaction and automatically begins a new one.
+Similarly, you can issue a rollback using `UnitOfWork.TransactionHandler.rollback()` which also begins a new transaction
+after rolling back the current one.
+
+`UnitOfWork` does *not* allow nesting:
+
+[source, java]
+----
+{
+  get("/nope", ctx -> require(UnitOfWork.class)
+      .apply(em -> {
+
+        // will lead to exception
+        require(UnitOfWork.class).accept(...);
+
+        return "ok";
+      }));
+}
+----
+
+Neither can it be used together with `SessionRequest` or `TransactionalRequest`:
+
+[source, java]
+----
+{
+  decorator(new TransactionalRequest());
+
+  // will lead to exception
+  get("/nope", ctx -> require(UnitOfWork.class)
+      .apply(em -> em.createQuery("from Pet", Pet.class).getResultList()));
+}
+----
+
 === Schema Creation
 
 Schema creation is controlled by the `hibernate.hbm2ddl.auto` property. The Hibernate module configure this property using the following rules:
diff --git a/docs/asciidoc/responses.adoc b/docs/asciidoc/responses.adoc
index 64a4d4050..401d78df0 100644
--- a/docs/asciidoc/responses.adoc
+++ b/docs/asciidoc/responses.adoc
@@ -118,9 +118,8 @@ automatically close the writer/stream:
 
 === File download
 
-The javadoc:FileDownload[] is used to generate file downloads, i.e. responses with 
-`Content-Disposition` header. You can use the convenience subclasses javadoc:AttachedFile[]
-or javadoc:InlineFile[] to set the header value to `attachment` or `inline` respectively.
+The javadoc:AttachedFile[] is used to generate file downloads, i.e. responses with 
+`Content-Disposition` header.
 
 .File download example
 [source,java,role="primary"]
@@ -131,9 +130,9 @@ or javadoc:InlineFile[] to set the header value to `attachment` or `inline` resp
     return new AttachedFile(source);               // <1>
   });
   
-  get("/view-stream", ctx -> {
+  get("/download-stream", ctx -> {
     InputStream source = ...;
-    return new InlineFile("myfile.txt", source); // <2>
+    return new AttachedFile("myfile.txt", source); // <2>
   });
 }
 ----
@@ -147,9 +146,9 @@ or javadoc:InlineFile[] to set the header value to `attachment` or `inline` resp
     AttachedFile(source)                // <1>
   }
 
-  get("/view-stream") {
+  get("/download-stream") {
     val source = ...
-    InlineFile("myfile.txt", source)  // <2>
+    AttachedFile("myfile.txt", source)  // <2>
   }
 }
 ----
@@ -157,39 +156,6 @@ or javadoc:InlineFile[] to set the header value to `attachment` or `inline` resp
 <1> Send a download from an `InputStream`
 <2> Send a download from a `File`
 
-Another possibility is to use one of the static builder methods of `FileDownload` and specify
-the download type (attachment or inline) later.
-
-.File download with builder method
-[source,java,role="primary"]
-----
-FileDownload.Builder produceDownload(Context ctx) {
-  return FileDownload.build(...);
-}
-
-{
-  get("/view", ctx -> produceDownload(ctx).inline());
-  
-  get("/download", ctx -> produceDownload(ctx).attachment());
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-fun produceDownload(ctx: Context) = FileDownload.build(...)
-
-{
-  get("/view") {
-    produceDownload(it).inline()
-  }
-
-  get("/download") {
-    produceDownload(it).attachment()
-  }
-}
-----
-
 === NonBlocking
 
 Non-blocking responses are a new feature of Jooby 2.x.
@@ -680,5 +646,5 @@ Family of send methods include:
 - javadoc:Context[send, java.nio.file.Path]
 - javadoc:Context[send, java.io.File]
 - javadoc:Context[send, java.nio.channels.FileChannel]
-- javadoc:Context[send, io.jooby.FileDownload]
+- javadoc:Context[send, io.jooby.AttachedFile]
 - javadoc:Context[send, io.jooby.StatusCode]
diff --git a/jooby/src/main/java/io/jooby/AttachedFile.java b/jooby/src/main/java/io/jooby/AttachedFile.java
index a19767d26..9252aeed8 100644
--- a/jooby/src/main/java/io/jooby/AttachedFile.java
+++ b/jooby/src/main/java/io/jooby/AttachedFile.java
@@ -5,18 +5,39 @@
  */
 package io.jooby;
 
+import org.apache.commons.io.FilenameUtils;
+
 import javax.annotation.Nonnull;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.file.Files;
 import java.nio.file.Path;
 
 /**
- * Represents a file attachment response.
+ * Represent a file attachment response.
  *
  * @author edgar
  * @since 2.0.0
  */
-public class AttachedFile extends FileDownload {
+public class AttachedFile {
+
+  private static final String CONTENT_DISPOSITION = "attachment;filename=\"%s\"";
+  private static final String FILENAME_STAR = ";filename*=%s''%s";
+
+  private static final String CHARSET = "UTF-8";
+
+  private final long fileSize;
+
+  private final MediaType contentType;
+
+  private final String fileName;
+
+  private final String contentDisposition;
+
+  private final InputStream content;
 
   /**
    * Creates a new file attachment.
@@ -26,7 +47,21 @@ public class AttachedFile extends FileDownload {
    * @param fileSize File size or <code>-1</code> if unknown.
    */
   public AttachedFile(@Nonnull InputStream content, @Nonnull String fileName, long fileSize) {
-    super(Mode.ATTACHMENT, content, fileName, fileSize);
+    try {
+      this.fileName = FilenameUtils.getName(fileName);
+      this.contentType = MediaType.byFile(this.fileName);
+      String filenameStar = URLEncoder.encode(this.fileName, CHARSET).replaceAll("\\+", "%20");
+      if (this.fileName.equals(filenameStar)) {
+        this.contentDisposition = String.format(CONTENT_DISPOSITION, this.fileName);
+      } else {
+        this.contentDisposition = String.format(CONTENT_DISPOSITION, this.fileName) + String
+            .format(FILENAME_STAR, CHARSET, filenameStar);
+      }
+      this.content = content;
+      this.fileSize = fileSize;
+    } catch (UnsupportedEncodingException x) {
+      throw SneakyThrows.propagate(x);
+    }
   }
 
   /**
@@ -36,7 +71,7 @@ public class AttachedFile extends FileDownload {
    * @param fileName Filename.
    */
   public AttachedFile(@Nonnull InputStream content, @Nonnull String fileName) {
-    super(Mode.ATTACHMENT, content, fileName);
+    this(content, fileName, -1);
   }
 
   /**
@@ -47,7 +82,7 @@ public class AttachedFile extends FileDownload {
    * @throws IOException For IO exception while reading file.
    */
   public AttachedFile(@Nonnull Path file, @Nonnull String fileName) throws IOException {
-    super(Mode.ATTACHMENT, file, fileName);
+    this(new FileInputStream(file.toFile()), fileName, Files.size(file));
   }
 
   /**
@@ -57,6 +92,54 @@ public class AttachedFile extends FileDownload {
    * @throws IOException For IO exception while reading file.
    */
   public AttachedFile(@Nonnull Path file) throws IOException {
-    super(Mode.ATTACHMENT, file);
+    this(file, file.getFileName().toString());
+  }
+
+  /**
+   * File size or <code>-1</code> if unknown.
+   *
+   * @return File size or <code>-1</code> if unknown.
+   */
+  public long getFileSize() {
+    return fileSize;
+  }
+
+  /**
+   * File content type.
+   *
+   * @return File content type.
+   */
+  public MediaType getContentType() {
+    return contentType;
+  }
+
+  /**
+   * File name.
+   * @return File name.
+   */
+  public String getFileName() {
+    return fileName;
+  }
+
+  /**
+   * Content disposition header.
+   *
+   * @return Content disposition header.
+   */
+  public String getContentDisposition() {
+    return contentDisposition;
+  }
+
+  /**
+   * File content.
+   *
+   * @return File content.
+   */
+  public InputStream stream() {
+    return content;
+  }
+
+  @Override public String toString() {
+    return fileName;
   }
 }
diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java
index bc01c134e..2bae63a54 100644
--- a/jooby/src/main/java/io/jooby/Context.java
+++ b/jooby/src/main/java/io/jooby/Context.java
@@ -1203,12 +1203,12 @@ public interface Context extends Registry {
   @Nonnull Context send(@Nonnull InputStream input);
 
   /**
-   * Send a file download response.
+   * Send a file attached response.
    *
-   * @param file File download.
+   * @param file Attached file.
    * @return This context.
    */
-  @Nonnull Context send(@Nonnull FileDownload file);
+  @Nonnull Context send(@Nonnull AttachedFile file);
 
   /**
    * Send a file response.
diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java
index 3ac09ebd8..7617b4bdf 100644
--- a/jooby/src/main/java/io/jooby/DefaultContext.java
+++ b/jooby/src/main/java/io/jooby/DefaultContext.java
@@ -506,7 +506,7 @@ public interface DefaultContext extends Context {
     return send(data, StandardCharsets.UTF_8);
   }
 
-  @Override default @Nonnull Context send(@Nonnull FileDownload file) {
+  @Override default @Nonnull Context send(@Nonnull AttachedFile file) {
     setResponseHeader("Content-Disposition", file.getContentDisposition());
     InputStream content = file.stream();
     long length = file.getFileSize();
diff --git a/jooby/src/main/java/io/jooby/FileDownload.java b/jooby/src/main/java/io/jooby/FileDownload.java
deleted file mode 100644
index adea4e6f3..000000000
--- a/jooby/src/main/java/io/jooby/FileDownload.java
+++ /dev/null
@@ -1,266 +0,0 @@
-/**
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby;
-
-import org.apache.commons.io.FilenameUtils;
-
-import javax.annotation.Nonnull;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-/**
- * Represents a file download.
- *
- * @author edgar
- * @author imeszaros
- * @since 2.9.0
- */
-public class FileDownload {
-
-  /**
-   * Download mode.
-   */
-  public enum Mode {
-
-    /** Value indicating that the file can be displayed inside the Web page, or as the Web page. */
-    INLINE("inline"),
-
-    /** Value indicating that the file should be downloaded; most browsers present a 'Save as' dialog. */
-    ATTACHMENT("attachment");
-
-    final String value;
-
-    Mode(String value) {
-      this.value = value;
-    }
-  }
-
-  private static final String CONTENT_DISPOSITION = "%s;filename=\"%s\"";
-  private static final String FILENAME_STAR = ";filename*=%s''%s";
-
-  private static final String CHARSET = "UTF-8";
-
-  private final long fileSize;
-
-  private final MediaType contentType;
-
-  private final String fileName;
-
-  private final String contentDisposition;
-
-  private final InputStream content;
-
-  /**
-   * Creates a new file attachment.
-   *
-   * @param mode Download mode.
-   * @param content File content.
-   * @param fileName Filename.
-   * @param fileSize File size or <code>-1</code> if unknown.
-   */
-  public FileDownload(Mode mode, @Nonnull InputStream content, @Nonnull String fileName, long fileSize) {
-    try {
-      this.fileName = FilenameUtils.getName(fileName);
-      this.contentType = MediaType.byFile(this.fileName);
-      String filenameStar = URLEncoder.encode(this.fileName, CHARSET).replaceAll("\\+", "%20");
-      if (this.fileName.equals(filenameStar)) {
-        this.contentDisposition = String.format(CONTENT_DISPOSITION, mode.value, this.fileName);
-      } else {
-        this.contentDisposition = String.format(CONTENT_DISPOSITION, mode.value, this.fileName) + String
-            .format(FILENAME_STAR, CHARSET, filenameStar);
-      }
-      this.content = content;
-      this.fileSize = fileSize;
-    } catch (UnsupportedEncodingException x) {
-      throw SneakyThrows.propagate(x);
-    }
-  }
-
-  /**
-   * Creates a new file attachment.
-   *
-   * @param mode Download mode.
-   * @param content File content.
-   * @param fileName Filename.
-   */
-  public FileDownload(Mode mode, @Nonnull InputStream content, @Nonnull String fileName) {
-    this(mode, content, fileName, -1);
-  }
-
-  /**
-   * Creates a new file attachment.
-   *
-   * @param mode Download mode.
-   * @param file File content.
-   * @param fileName Filename.
-   * @throws IOException For IO exception while reading file.
-   */
-  public FileDownload(Mode mode, @Nonnull Path file, @Nonnull String fileName) throws IOException {
-    this(mode, new FileInputStream(file.toFile()), fileName, Files.size(file));
-  }
-
-  /**
-   * Creates a new file attachment.
-   *
-   * @param mode Download mode.
-   * @param file File content.
-   * @throws IOException For IO exception while reading file.
-   */
-  public FileDownload(Mode mode, @Nonnull Path file) throws IOException {
-    this(mode, file, file.getFileName().toString());
-  }
-
-  /**
-   * File size or <code>-1</code> if unknown.
-   *
-   * @return File size or <code>-1</code> if unknown.
-   */
-  public long getFileSize() {
-    return fileSize;
-  }
-
-  /**
-   * File content type.
-   *
-   * @return File content type.
-   */
-  public MediaType getContentType() {
-    return contentType;
-  }
-
-  /**
-   * File name.
-   * @return File name.
-   */
-  public String getFileName() {
-    return fileName;
-  }
-
-  /**
-   * Content disposition header.
-   *
-   * @return Content disposition header.
-   */
-  public String getContentDisposition() {
-    return contentDisposition;
-  }
-
-  /**
-   * File content.
-   *
-   * @return File content.
-   */
-  public InputStream stream() {
-    return content;
-  }
-
-  @Override public String toString() {
-    return fileName;
-  }
-
-  /**
-   * Allows creating a {@link FileDownload} with
-   * the specified {@link Mode}.
-   */
-  public interface Builder {
-
-    /**
-     * Creates a {@link FileDownload} with
-     * the specified {@link Mode}.
-     *
-     * @param mode the {@link Mode}
-     * @return a {@link FileDownload} with the specified mode
-     */
-    FileDownload build(Mode mode);
-
-    /**
-     * Creates an attached {@link FileDownload}.
-     *
-     * @return a {@link FileDownload} with {@link Mode#ATTACHMENT}
-     */
-    default FileDownload attachment() {
-      return build(Mode.ATTACHMENT);
-    }
-
-    /**
-     * Creates an inline {@link FileDownload}.
-     *
-     * @return a {@link FileDownload} with {@link Mode#INLINE}
-     */
-    default FileDownload inline() {
-      return build(Mode.INLINE);
-    }
-  }
-
-  /**
-   * Creates a builder with the specified content which can be used to create
-   * either a {@link FileDownload} with any {@link Mode}.
-   *
-   * @param content File content.
-   * @param fileName Filename.
-   * @param fileSize File size or <code>-1</code> if unknown.
-   *
-   * @return a {@link Builder} with the specified content
-   */
-  public static Builder build(@Nonnull InputStream content, @Nonnull String fileName, long fileSize) {
-    return mode -> new FileDownload(mode, content, fileName, fileSize);
-  }
-
-  /**
-   * Creates a builder with the specified content which can be used to create
-   * either a {@link FileDownload} with any {@link Mode}.
-   *
-   * @param content File content.
-   * @param fileName Filename.
-   *
-   * @return a {@link Builder} with the specified content
-   */
-  public static Builder build(@Nonnull InputStream content, @Nonnull String fileName) {
-    return mode -> new FileDownload(mode, content, fileName);
-  }
-
-  /**
-   * Creates a builder with the specified content which can be used to create
-   * either a {@link FileDownload} with any {@link Mode}.
-   *
-   * @param file File content.
-   * @param fileName Filename.
-   *
-   * @return a {@link Builder} with the specified content
-   */
-  public static Builder build(@Nonnull Path file, @Nonnull String fileName) {
-    return mode -> {
-      try {
-        return new FileDownload(mode, file, fileName);
-      } catch (IOException e) {
-        throw SneakyThrows.propagate(e);
-      }
-    };
-  }
-
-  /**
-   * Creates a builder with the specified content which can be used to create
-   * either a {@link FileDownload} with any {@link Mode}.
-   *
-   * @param file File content.
-   *
-   * @return a {@link Builder} with the specified content
-   */
-  public static Builder build(@Nonnull Path file) {
-    return mode -> {
-      try {
-        return new FileDownload(mode, file);
-      } catch (IOException e) {
-        throw SneakyThrows.propagate(e);
-      }
-    };
-  }
-}
diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java
index 5b2363c28..bed12842c 100644
--- a/jooby/src/main/java/io/jooby/ForwardingContext.java
+++ b/jooby/src/main/java/io/jooby/ForwardingContext.java
@@ -578,7 +578,7 @@ public class ForwardingContext implements Context {
     return this;
   }
 
-  @Nonnull @Override public Context send(@Nonnull FileDownload file) {
+  @Nonnull @Override public Context send(@Nonnull AttachedFile file) {
     ctx.send(file);
     return this;
   }
diff --git a/jooby/src/main/java/io/jooby/InlineFile.java b/jooby/src/main/java/io/jooby/InlineFile.java
deleted file mode 100644
index 7e9d02a9c..000000000
--- a/jooby/src/main/java/io/jooby/InlineFile.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Jooby https://jooby.io
- * Apache License Version 2.0 https://jooby.io/LICENSE.txt
- * Copyright 2014 Edgar Espina
- */
-package io.jooby;
-
-import javax.annotation.Nonnull;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Path;
-
-/**
- * Represents an inline file response.
- *
- * @author edgar
- * @since 2.0.0
- */
-public class InlineFile extends FileDownload {
-
-  /**
-   * Creates a new inline file.
-   *
-   * @param content File content.
-   * @param fileName Filename.
-   * @param fileSize File size or <code>-1</code> if unknown.
-   */
-  public InlineFile(@Nonnull InputStream content, @Nonnull String fileName, long fileSize) {
-    super(Mode.INLINE, content, fileName, fileSize);
-  }
-
-  /**
-   * Creates a new inline file.
-   *
-   * @param content File content.
-   * @param fileName Filename.
-   */
-  public InlineFile(@Nonnull InputStream content, @Nonnull String fileName) {
-    super(Mode.INLINE, content, fileName);
-  }
-
-  /**
-   * Creates a new inline file.
-   *
-   * @param file File content.
-   * @param fileName Filename.
-   * @throws IOException For IO exception while reading file.
-   */
-  public InlineFile(@Nonnull Path file, @Nonnull String fileName) throws IOException {
-    super(Mode.INLINE, file, fileName);
-  }
-
-  /**
-   * Creates a new inline file.
-   *
-   * @param file File content.
-   * @throws IOException For IO exception while reading file.
-   */
-  public InlineFile(@Nonnull Path file) throws IOException {
-    super(Mode.INLINE, file);
-  }
-}
diff --git a/jooby/src/main/java/io/jooby/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java
index 3010381d7..5563f5383 100644
--- a/jooby/src/main/java/io/jooby/internal/HeadContext.java
+++ b/jooby/src/main/java/io/jooby/internal/HeadContext.java
@@ -5,8 +5,8 @@
  */
 package io.jooby.internal;
 
+import io.jooby.AttachedFile;
 import io.jooby.Context;
-import io.jooby.FileDownload;
 import io.jooby.ForwardingContext;
 import io.jooby.MediaType;
 import io.jooby.MessageEncoder;
@@ -80,7 +80,7 @@ public class HeadContext extends ForwardingContext {
     }
   }
 
-  @Nonnull @Override public Context send(@Nonnull FileDownload file) {
+  @Nonnull @Override public Context send(@Nonnull AttachedFile file) {
     ctx.setResponseLength(file.getFileSize());
     ctx.setResponseType(file.getContentType());
     checkSizeHeaders();
diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java
index c4fe4b64e..d2539cf9a 100644
--- a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java
+++ b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java
@@ -5,8 +5,8 @@
  */
 package io.jooby.internal;
 
+import io.jooby.AttachedFile;
 import io.jooby.Context;
-import io.jooby.FileDownload;
 import io.jooby.MessageEncoder;
 import io.jooby.ModelAndView;
 import io.jooby.StatusCode;
@@ -71,9 +71,9 @@ public class HttpMessageEncoder implements MessageEncoder {
       ctx.send((Path) value);
       return null;
     }
-    /** FileDownload: */
-    if (value instanceof FileDownload) {
-      ctx.send((FileDownload) value);
+    /** Attached file: */
+    if (value instanceof AttachedFile) {
+      ctx.send((AttachedFile) value);
       return null;
     }
     /** Strings: */
diff --git a/jooby/src/main/java/io/jooby/internal/Pipeline.java b/jooby/src/main/java/io/jooby/internal/Pipeline.java
index ec44b0397..f1468bcac 100644
--- a/jooby/src/main/java/io/jooby/internal/Pipeline.java
+++ b/jooby/src/main/java/io/jooby/internal/Pipeline.java
@@ -5,9 +5,9 @@
  */
 package io.jooby.internal;
 
+import io.jooby.AttachedFile;
 import io.jooby.Context;
 import io.jooby.ExecutionMode;
-import io.jooby.FileDownload;
 import io.jooby.Reified;
 import io.jooby.ResponseHandler;
 import io.jooby.Route;
@@ -147,8 +147,8 @@ public class Pipeline {
         .isAssignableFrom(type)) {
       return next(mode, executor, decorate(route, new SendFileChannel(route.getPipeline())), true);
     }
-    /** FileDownload: */
-    if (FileDownload.class.isAssignableFrom(type)) {
+    /** Attached file: */
+    if (AttachedFile.class.isAssignableFrom(type)) {
       return next(mode, executor, decorate(route, new SendAttachment(route.getPipeline())), true);
     }
     /** Strings: */
diff --git a/jooby/src/main/java/io/jooby/internal/ReadOnlyContext.java b/jooby/src/main/java/io/jooby/internal/ReadOnlyContext.java
index 75dceacfd..b34b5e82c 100644
--- a/jooby/src/main/java/io/jooby/internal/ReadOnlyContext.java
+++ b/jooby/src/main/java/io/jooby/internal/ReadOnlyContext.java
@@ -5,9 +5,9 @@
  */
 package io.jooby.internal;
 
+import io.jooby.AttachedFile;
 import io.jooby.Context;
 import io.jooby.Cookie;
-import io.jooby.FileDownload;
 import io.jooby.ForwardingContext;
 import io.jooby.MediaType;
 import io.jooby.Sender;
@@ -58,7 +58,7 @@ public class ReadOnlyContext extends ForwardingContext {
     throw new IllegalStateException(MESSAGE);
   }
 
-  @Nonnull @Override public Context send(@Nonnull FileDownload file) {
+  @Nonnull @Override public Context send(@Nonnull AttachedFile file) {
     throw new IllegalStateException(MESSAGE);
   }
 
diff --git a/jooby/src/main/java/io/jooby/internal/handler/SendAttachment.java b/jooby/src/main/java/io/jooby/internal/handler/SendAttachment.java
index c04b0d52c..60ce5eb01 100644
--- a/jooby/src/main/java/io/jooby/internal/handler/SendAttachment.java
+++ b/jooby/src/main/java/io/jooby/internal/handler/SendAttachment.java
@@ -5,8 +5,8 @@
  */
 package io.jooby.internal.handler;
 
+import io.jooby.AttachedFile;
 import io.jooby.Context;
-import io.jooby.FileDownload;
 import io.jooby.Route;
 
 import javax.annotation.Nonnull;
@@ -24,7 +24,7 @@ public class SendAttachment implements LinkedHandler {
       if (ctx.isResponseStarted()) {
         return result;
       }
-      return ctx.send(((FileDownload) result));
+      return ctx.send(((AttachedFile) result));
     } catch (Throwable x) {
       return ctx.sendError(x);
     }
diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml
index 2824944be..9905df798 100644
--- a/modules/jooby-bom/pom.xml
+++ b/modules/jooby-bom/pom.xml
@@ -33,7 +33,7 @@
   <cron-utils.version>9.1.1</cron-utils.version>
   <ebean.version>12.3.7</ebean.version>
   <exec-maven-plugin.version>3.0.0</exec-maven-plugin.version>
-  <flyway.version>6.5.5</flyway.version>
+  <flyway.version>6.5.4</flyway.version>
   <freemarker.version>2.3.30</freemarker.version>
   <frontend-maven-plugin.version>1.10.0</frontend-maven-plugin.version>
   <frontend-plugin-core.version>1.10.0</frontend-plugin-core.version>
@@ -46,7 +46,7 @@
   <guice.version>4.2.3</guice.version>
   <h2.version>1.4.200</h2.version>
   <handlebars.version>4.2.0</handlebars.version>
-  <hibernate.version>5.4.20.Final</hibernate.version>
+  <hibernate.version>5.4.19.Final</hibernate.version>
   <jackson.version>2.11.2</jackson.version>
   <jacoco-maven-plugin.version>0.8.5</jacoco-maven-plugin.version>
   <jacoco.version>0.8.5</jacoco.version>
@@ -103,7 +103,7 @@
   <pebble.version>3.1.4</pebble.version>
   <plexus-utils.version>3.3.0</plexus-utils.version>
   <quartz.version>2.3.2</quartz.version>
-  <reactor.version>3.3.9.RELEASE</reactor.version>
+  <reactor.version>3.3.8.RELEASE</reactor.version>
   <redoc.version>2.0.0-rc.20</redoc.version>
   <rest-assured.version>4.3.0</rest-assured.version>
   <rocker.version>1.3.0</rocker.version>
diff --git a/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java b/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java
index c68a27ed6..cd78a8ae9 100644
--- a/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java
+++ b/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java
@@ -11,6 +11,7 @@ import io.jooby.Extension;
 import io.jooby.Jooby;
 import io.jooby.ServiceKey;
 import io.jooby.ServiceRegistry;
+import io.jooby.internal.hibernate.UnitOfWorkProvider;
 import io.jooby.internal.hibernate.ScanEnvImpl;
 import io.jooby.internal.hibernate.SessionServiceProvider;
 import org.hibernate.Session;
@@ -291,6 +292,11 @@ public class HibernateModule implements Extension {
     registry.putIfAbsent(SessionProvider.class, sessionBuilder);
     registry.put(ServiceKey.key(SessionProvider.class, name), sessionBuilder);
 
+    /** UnitOfWork Provider: */
+    UnitOfWorkProvider unitOfWorkProvider = new UnitOfWorkProvider(sf, sessionBuilder);
+    registry.putIfAbsent(UnitOfWork.class, unitOfWorkProvider);
+    registry.put(ServiceKey.key(UnitOfWork.class, name), unitOfWorkProvider);
+
     application.onStop(sf::close);
   }
 
diff --git a/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/UnitOfWork.java b/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/UnitOfWork.java
new file mode 100644
index 000000000..c3180761b
--- /dev/null
+++ b/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/UnitOfWork.java
@@ -0,0 +1,145 @@
+/**
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.hibernate;
+
+import io.jooby.SneakyThrows;
+
+import javax.persistence.EntityManager;
+
+/**
+ * Allows you to open a JPA session on demand by acquiring an instance
+ * of a class implementing this interface via the service registry or DI.
+ * <p>
+ * Usage:
+ *
+ * <pre>{@code
+ * {
+ *   get("/pets", ctx -> require(UnitOfWork.class)
+ *       .apply(em -> em.createQuery("from Pet", Pet.class).getResultList()));
+ * }
+ * }</pre>
+ *
+ * Automatically manages the lifecycle of the {@link EntityManager} instance
+ * and transactions. After the code block passed to {@code apply} or {@code accept}
+ * returns the transaction is being committed and the {@link EntityManager} closed.
+ * <p>
+ * If the code block throws an exception, the transaction is rolled back, and the
+ * {@link EntityManager} is released as well.
+ * <p>
+ * You may access a {@link TransactionHandler} instance to be able to work with
+ * multiple transactions:
+ *
+ * <pre>{@code
+ * {
+ *   get("/update", ctx -> require(UnitOfWork.class)
+ *       .apply((em, txh) -> {
+ *         em.createQuery("from Pet", Pet.class).getResultList().forEach(pet -> {
+ *           pet.setName(pet.getName() + " Updated");
+ *           txh.commit(); // update each entity in a separate transaction
+ *         });
+ *         return "ok";
+ *       }));
+ * }
+ * }</pre>
+ *
+ * A call to {@link TransactionHandler#commit()} commits the current transaction
+ * and automatically begins a new one. Similarly, you can issue a rollback using
+ * {@link TransactionHandler#rollback()} which also begins a new transaction
+ * after rolling back the current one.
+ * <p>
+ * {@link UnitOfWork} does NOT allow nesting:
+ *
+ * <pre>{@code
+ * {
+ *   get("/nope", ctx -> require(UnitOfWork.class)
+ *       .apply(em -> {
+ *
+ *         // will lead to exception
+ *         require(UnitOfWork.class).accept(...);
+ *
+ *         return "ok";
+ *       }));
+ * }
+ * }</pre>
+ *
+ * Neither can it be used together with {@link SessionRequest} or {@link TransactionalRequest}:
+ *
+ * <pre>{@code
+ * {
+ *   decorator(new TransactionalRequest());
+ *
+ *   // will lead to exception
+ *   get("/nope", ctx -> require(UnitOfWork.class)
+ *       .apply(em -> em.createQuery("from Pet", Pet.class).getResultList()));
+ * }
+ * }</pre>
+ */
+public interface UnitOfWork {
+
+  /**
+   * Allows committing or rolling back the current transaction, immediately
+   * beginning a new one.
+   */
+  interface TransactionHandler {
+
+    /**
+     * Commits the current transaction and begins a new one.
+     */
+    void commit();
+
+    /**
+     * Rolls the current transaction back and begins a new one.
+     */
+    void rollback();
+  }
+
+  /**
+   * Runs the specified code block passing a reference to an {@link EntityManager} to it.
+   *
+   * @param callback the block to execute
+   */
+  default void accept(SneakyThrows.Consumer<EntityManager> callback) {
+    apply(((entityManager) -> {
+      callback.accept(entityManager);
+      return null;
+    }));
+  }
+
+  /**
+   * Runs the specified code block passing a reference to an {@link EntityManager} and
+   * a {@link TransactionHandler} to it.
+   *
+   * @param callback the block to execute
+   */
+  default void accept(SneakyThrows.Consumer2<EntityManager, TransactionHandler> callback) {
+    apply(((entityManager, transactionHandler) -> {
+      callback.accept(entityManager, transactionHandler);
+      return null;
+    }));
+  }
+
+  /**
+   * Runs the specified code block passing a reference to an {@link EntityManager} to it,
+   * and returns it's result.
+   *
+   * @param <T> type of return value
+   * @param callback the block to execute
+   * @return the result of the callback
+   */
+  default <T> T apply(SneakyThrows.Function<EntityManager, T> callback) {
+    return apply(((entityManager, transactionHandler) -> callback.apply(entityManager)));
+  }
+
+  /**
+   * Runs the specified code block passing a reference to an {@link EntityManager} and
+   * a {@link TransactionHandler} to it, and returns it's result.
+   *
+   * @param <T> type of return value
+   * @param callback the block to execute
+   * @return the result of the callback
+   */
+  <T> T apply(SneakyThrows.Function2<EntityManager, TransactionHandler, T> callback);
+}
diff --git a/modules/jooby-hibernate/src/main/java/io/jooby/internal/hibernate/UnitOfWorkImpl.java b/modules/jooby-hibernate/src/main/java/io/jooby/internal/hibernate/UnitOfWorkImpl.java
new file mode 100644
index 000000000..f72783a29
--- /dev/null
+++ b/modules/jooby-hibernate/src/main/java/io/jooby/internal/hibernate/UnitOfWorkImpl.java
@@ -0,0 +1,111 @@
+/**
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.internal.hibernate;
+
+import io.jooby.SneakyThrows;
+import io.jooby.hibernate.UnitOfWork;
+import org.hibernate.*;
+import org.hibernate.context.internal.ManagedSessionContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.persistence.EntityManager;
+
+import static java.util.Objects.requireNonNull;
+
+public class UnitOfWorkImpl implements UnitOfWork {
+
+  private final Session session;
+  private final Logger logger = LoggerFactory.getLogger(UnitOfWorkImpl.class);
+
+  public UnitOfWorkImpl(Session session) {
+    this.session = requireNonNull(session);
+    bind(session);
+    session.setHibernateFlushMode(FlushMode.AUTO);
+  }
+
+  private void bind(Session session) {
+    logger.debug("session bound: {}", oid(session));
+    ManagedSessionContext.bind(session);
+  }
+
+  private void unbind(SessionFactory sessionFactory) {
+    final Session session = ManagedSessionContext.unbind(sessionFactory);
+    logger.debug("session unbound: {}", oid(session));
+  }
+
+  private String oid(Object value) {
+    return Integer.toHexString(System.identityHashCode(value));
+  }
+
+  private void begin() {
+    final Transaction tx = session.getTransaction();
+    logger.debug("begin transaction: {}(trx@{})", oid(session), oid(tx));
+    tx.begin();
+  }
+
+  private void commit() {
+    logger.debug("flushing session: {}", oid(session));
+    session.flush();
+
+    final Transaction tx = session.getTransaction();
+    logger.debug("commit transaction: {}(trx@{})", oid(session), oid(tx));
+    tx.commit();
+  }
+
+  private void rollback() {
+    final Transaction tx = session.getTransaction();
+    logger.debug("rollback transaction: {}(trx@{})", oid(session), oid(tx));
+    tx.rollback();
+  }
+
+  @Override
+  public <T> T apply(SneakyThrows.Function2<EntityManager, TransactionHandler, T> callback) {
+    if (!session.isOpen()) {
+      throw new IllegalStateException("Session is not open.");
+    }
+
+    try {
+      begin();
+
+      final T result = callback.apply(session, new TransactionHandler() {
+
+        @Override
+        public void commit() {
+          UnitOfWorkImpl.this.commit();
+          begin();
+        }
+
+        @Override
+        public void rollback() {
+          UnitOfWorkImpl.this.rollback();
+          begin();
+        }
+      });
+
+      commit();
+
+      return result;
+    } catch (Throwable t) {
+      try {
+        rollback();
+      } catch (Throwable th) {
+        logger.error("failed to rollback transaction: {}", oid(session), th);
+      }
+
+      throw SneakyThrows.propagate(t);
+    } finally {
+      try {
+        session.close();
+        logger.debug("session closed: {}", oid(session));
+      } catch (HibernateException e) {
+        logger.error("session.close() resulted in exception: {}", oid(session), e);
+      } finally {
+        unbind(session.getSessionFactory());
+      }
+    }
+  }
+}
diff --git a/modules/jooby-hibernate/src/main/java/io/jooby/internal/hibernate/UnitOfWorkProvider.java b/modules/jooby-hibernate/src/main/java/io/jooby/internal/hibernate/UnitOfWorkProvider.java
new file mode 100644
index 000000000..15fa0797a
--- /dev/null
+++ b/modules/jooby-hibernate/src/main/java/io/jooby/internal/hibernate/UnitOfWorkProvider.java
@@ -0,0 +1,39 @@
+/**
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.internal.hibernate;
+
+import io.jooby.hibernate.UnitOfWork;
+import io.jooby.hibernate.SessionProvider;
+import io.jooby.hibernate.SessionRequest;
+import io.jooby.hibernate.TransactionalRequest;
+import org.hibernate.SessionFactory;
+import org.hibernate.context.internal.ManagedSessionContext;
+
+import javax.inject.Provider;
+
+public class UnitOfWorkProvider implements Provider<UnitOfWork> {
+
+  private final SessionFactory sessionFactory;
+  private final SessionProvider sessionProvider;
+
+  public UnitOfWorkProvider(SessionFactory sessionFactory, SessionProvider sessionProvider) {
+    this.sessionFactory = sessionFactory;
+    this.sessionProvider = sessionProvider;
+  }
+
+  @Override
+  public UnitOfWork get() {
+    if (ManagedSessionContext.hasBind(sessionFactory)) {
+      throw new IllegalStateException("A session is already bound to the current thread. Don't nest "
+          + UnitOfWork.class.getSimpleName() + " invocations or don't use "
+          + UnitOfWork.class.getSimpleName() + " together with "
+          + SessionRequest.class.getSimpleName() + " or "
+          + TransactionalRequest.class.getSimpleName() + ".");
+    }
+
+    return new UnitOfWorkImpl(sessionProvider.newSession(sessionFactory.withOptions()));
+  }
+}
diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java
index efbf1328a..fc72353b3 100644
--- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java
+++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java
@@ -175,10 +175,6 @@ public class AnnotationParser {
       "kotlin.coroutines.Continuation"
   ).stream().collect(Collectors.toSet());
 
-  static final Set<String> IGNORED_ANNOTATIONS = Arrays.asList(
-      ContextParam.class.getName()
-  ).stream().collect(Collectors.toSet());
-
   public static List<OperationExt> parse(ParserContext ctx, String prefix,
       Signature signature, MethodInsnNode node) {
     if (signature.matches(Class.class) ||
@@ -351,13 +347,6 @@ public class AnnotationParser {
         } else {
           annotations = Collections.emptyList();
         }
-
-        if (annotations != null && annotations.stream()
-            .anyMatch(n -> IGNORED_ANNOTATIONS.contains(ASMType.parse(n.desc)))) {
-
-          continue;
-        }
-
         ParamType paramType = ParamType.find(annotations);
 
         /** Required: */
diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ReturnTypeParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ReturnTypeParser.java
index fbb546546..035be5e56 100644
--- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ReturnTypeParser.java
+++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ReturnTypeParser.java
@@ -22,7 +22,6 @@ import org.objectweb.asm.tree.MethodNode;
 import org.objectweb.asm.tree.TypeInsnNode;
 import org.objectweb.asm.tree.VarInsnNode;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -232,7 +231,13 @@ public class ReturnTypeParser {
       return Object.class.getName();
     }
     Type returnType = Type.getReturnType(node.desc);
-    return classMethods(ctx, node.owner).stream()
+    ClassNode classNode;
+    try {
+      classNode = ctx.classNode(Type.getObjectType(node.owner));
+    } catch (Exception x) {
+      return returnType.getClassName();
+    }
+    return classNode.methods.stream()
         .filter(m -> m.name.equals(node.name) && m.desc.equals(node.desc))
         .findFirst()
         .map(m -> Optional.ofNullable(m.signature)
@@ -246,21 +251,6 @@ public class ReturnTypeParser {
         .orElse(returnType.getClassName());
   }
 
-  private static List<MethodNode> classMethods(ParserContext ctx, String owner) {
-    ClassNode classNode = ctx.classNodeOrNull(Type.getObjectType(owner));
-    if (classNode == null) {
-      return Collections.emptyList();
-    }
-    List<MethodNode> result = new ArrayList<>();
-    result.addAll(classNode.methods);
-    if (classNode.interfaces != null) {
-      for (String anInterface : classNode.interfaces) {
-        result.addAll(classMethods(ctx, anInterface));
-      }
-    }
-    return result;
-  }
-
   private static String localVariable(final ParserContext ctx, final MethodNode m,
       final VarInsnNode varInsn) {
     int opcode = varInsn.getOpcode();
@@ -272,7 +262,6 @@ public class ReturnTypeParser {
           .orElse(null);
       if (var != null) {
         if (var.signature == null) {
-          /** Kotlin traversal: */
           Optional<AbstractInsnNode> kt = InsnSupport.prev(varInsn).filter(kotlinIntrinsics())
               .findFirst();
           if (kt.isPresent()) {
@@ -288,37 +277,11 @@ public class ReturnTypeParser {
                         && !internalName.equals("kotlin/jvm/functions/Function1")
                         && !internalName.equals("io/jooby/HandlerContext")
                 );
-                if (!type.equals(Object.class.getName()) && !type.equals(void.class.getName())) {
-                  return type;
-                }
-              }
-            }
-          }
-
-          String type = ASMType.parse(var.desc);
-          if (type.startsWith("java.util.")) {
-            /** Try to find originating call to figure out element type <T> */
-            VarInsnNode astore = InsnSupport.prev(varInsn)
-                .filter(VarInsnNode.class::isInstance)
-                .map(VarInsnNode.class::cast)
-                .filter(varIns -> varIns.getOpcode() == Opcodes.ASTORE && varIns.var == var.index)
-                .findFirst()
-                .orElse(null);
-            if (astore != null) {
-              MethodInsnNode methodCall = InsnSupport.prev(astore)
-                  .filter(it -> (it instanceof MethodInsnNode) && !kotlinIntrinsics().test(it))
-                  .map(MethodInsnNode.class::cast)
-                  .findFirst()
-                  .orElse(null);
-              if (methodCall != null) {
-                String returnType = fromMethodCall(ctx, methodCall);
-                if (!returnType.equals(Object.class.getName()) && !returnType.equals(void.class.getName())) {
-                  return returnType;
-                }
+                return type;
               }
             }
           }
-          return type;
+          return ASMType.parse(var.desc);
         }
         return ASMType.parse(var.signature);
       }
diff --git a/modules/jooby-redoc/package-lock.json b/modules/jooby-redoc/package-lock.json
index 096a8c943..1c11c47cd 100644
--- a/modules/jooby-redoc/package-lock.json
+++ b/modules/jooby-redoc/package-lock.json
@@ -36,9 +36,9 @@
       }
     },
     "@babel/runtime": {
-      "version": "7.11.2",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
-      "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
+      "version": "7.11.1",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.1.tgz",
+      "integrity": "sha512-nH5y8fLvVl3HAb+ezbgcgwrH8QbClWo8xzkOu7+oyqngo3EVorwpWJQaqXPjGRpfj7mQvsJCl/S8knkfkPWqrw==",
       "requires": {
         "regenerator-runtime": "^0.13.4"
       }
@@ -391,9 +391,9 @@
       "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
     },
     "eventemitter3": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.5.tgz",
-      "integrity": "sha512-QR0rh0YiPuxuDQ6+T9GAO/xWTExXpxIes1Nl9RykNGTnE1HJmkuEfxJH9cubjIOQZ/GH4qNBR4u8VSHaKiWs4g=="
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
+      "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ=="
     },
     "fast-deep-equal": {
       "version": "1.1.0",
@@ -496,10 +496,10 @@
         "esprima": "^4.0.0"
       }
     },
-    "json-parse-even-better-errors": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz",
-      "integrity": "sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q=="
+    "json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
     },
     "json-pointer": {
       "version": "0.6.0",
@@ -557,9 +557,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -657,13 +657,13 @@
       }
     },
     "oas-resolver": {
-      "version": "2.4.3",
-      "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.4.3.tgz",
-      "integrity": "sha512-+66lGk5GewIXoIkqxfeWp89M/SgQT67oITiuG6qNIUuq1gczIIy68ZrMiE+gyDZY2IAdVxY9k73o9nHBnKwHPA==",
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.4.2.tgz",
+      "integrity": "sha512-iJo7wE/MhuCJefkcpCS/NlE8MunRgRvgPozpeLSZUg0zmU8PBkzUwdtzpmjGDd7QjEuUi0SZ/y1wIrFIH+FNiA==",
       "requires": {
         "node-fetch-h2": "^2.3.0",
         "oas-kit-common": "^1.0.8",
-        "reftools": "^1.1.5",
+        "reftools": "^1.1.4",
         "yaml": "^1.8.3",
         "yargs": "^15.3.1"
       }
@@ -674,18 +674,18 @@
       "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ=="
     },
     "oas-validator": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-4.0.8.tgz",
-      "integrity": "sha512-bIt8erTyclF7bkaySTtQ9sppqyVc+mAlPi7vPzCLVHJsL9nrivQjc/jHLX/o+eGbxHd6a6YBwuY/Vxa6wGsiuw==",
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-4.0.7.tgz",
+      "integrity": "sha512-ppSW68iIIhvzFwSvY51NJPLM0uFjkHKAdoXKO+Pq6Ej1qU5Nvi9I3dQt6W8y/B+UYIP8yXr9YTEuvzG7sQH/ww==",
       "requires": {
         "ajv": "^5.5.2",
         "better-ajv-errors": "^0.6.7",
         "call-me-maybe": "^1.0.1",
         "oas-kit-common": "^1.0.8",
         "oas-linter": "^3.1.3",
-        "oas-resolver": "^2.4.3",
+        "oas-resolver": "^2.4.2",
         "oas-schema-walker": "^1.1.5",
-        "reftools": "^1.1.5",
+        "reftools": "^1.1.4",
         "should": "^13.2.1",
         "yaml": "^1.8.3"
       }
@@ -741,13 +741,13 @@
       }
     },
     "parse-json": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
-      "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz",
+      "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==",
       "requires": {
         "@babel/code-frame": "^7.0.0",
         "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
+        "json-parse-better-errors": "^1.0.1",
         "lines-and-columns": "^1.1.6"
       }
     },
@@ -820,9 +820,9 @@
       }
     },
     "redoc": {
-      "version": "2.0.0-rc.38",
-      "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.38.tgz",
-      "integrity": "sha512-y5rIiAqrDGG686kmzI0XsYsh10sFBmJ6kQS0q18xhaihCkAxHM002yXCd7gnacYzYH4i8WD4HdF7aefbUxFRzw==",
+      "version": "2.0.0-rc.36",
+      "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.36.tgz",
+      "integrity": "sha512-vTK1slMn1FcV4QwiBxFL6adIzYoOPzrkRsVyedGj4SrqjIJXuQ5HWVsxpMGoru45tgp3bs7Jy3TBOcEdOYjbbg==",
       "requires": {
         "@types/node": "^13.11.1",
         "classnames": "^2.2.6",
@@ -851,9 +851,9 @@
       }
     },
     "reftools": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.5.tgz",
-      "integrity": "sha512-o7RX5wMzITYj+8P8oaccFH2xYu5nQ63TH0d73Ce5YgpiY9NyFytaQfeg1p1Z5T4yfjKSj0sYuUscBqZgkLsooQ=="
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.4.tgz",
+      "integrity": "sha512-Y8VEk3OXPwuU+ZAAPiR0YhYy9iBSO3NBRnXHGfqW6c2mo2V+fQ0cwRA38Ny6z9ZzcAo6HjmVvAri+wz3+8fsdQ=="
     },
     "regenerator-runtime": {
       "version": "0.13.7",
@@ -1004,19 +1004,19 @@
       }
     },
     "swagger2openapi": {
-      "version": "6.2.3",
-      "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-6.2.3.tgz",
-      "integrity": "sha512-cUUktzLpK69UwpMbcTzjMw2ns9RZChfxh56AHv6+hTx3StPOX2foZjPgds3HlJcINbxosYYBn/D3cG8nwcCWwQ==",
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-6.2.2.tgz",
+      "integrity": "sha512-A8RWwzkymhF/ZfO0AylEZ2eCaN2jvAJU3bdtXQzkW5QvuyBMUBcyfB2tkHMTpXWYDmt7uKHwPGRZ0doPejtARA==",
       "requires": {
         "better-ajv-errors": "^0.6.1",
         "call-me-maybe": "^1.0.1",
         "node-fetch-h2": "^2.3.0",
         "node-readfiles": "^0.2.0",
         "oas-kit-common": "^1.0.8",
-        "oas-resolver": "^2.4.3",
+        "oas-resolver": "^2.4.2",
         "oas-schema-walker": "^1.1.5",
-        "oas-validator": "^4.0.8",
-        "reftools": "^1.1.5",
+        "oas-validator": "^4.0.7",
+        "reftools": "^1.1.4",
         "yaml": "^1.8.3",
         "yargs": "^15.3.1"
       }
@@ -1041,9 +1041,9 @@
       }
     },
     "tslib": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
-      "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
+      "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
     },
     "url-template": {
       "version": "2.0.8",
diff --git a/modules/jooby-redoc/package.json b/modules/jooby-redoc/package.json
index a76a53356..afbc31f15 100644
--- a/modules/jooby-redoc/package.json
+++ b/modules/jooby-redoc/package.json
@@ -4,6 +4,6 @@
   "private": true,
   "license": "ASF",
   "dependencies": {
-    "redoc": "^2.0.0-rc.38"
+    "redoc": "^2.0.0-rc.36"
   }
 }
diff --git a/modules/jooby-swagger-ui/package-lock.json b/modules/jooby-swagger-ui/package-lock.json
index e36683cad..53cf17d82 100644
--- a/modules/jooby-swagger-ui/package-lock.json
+++ b/modules/jooby-swagger-ui/package-lock.json
@@ -5,9 +5,9 @@
   "requires": true,
   "dependencies": {
     "swagger-ui-dist": {
-      "version": "3.32.4",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.32.4.tgz",
-      "integrity": "sha512-3qUqK131a5nqGdDJhLflTNzvrjZgjBlINYNx+Jm5lw/Va88Lcu5iyjUupY3Js/Kf326z1XtXkrr6TbvE6r925g=="
+      "version": "3.32.1",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.32.1.tgz",
+      "integrity": "sha512-CaIKxDo91McgoesukS5v3nwQ8iur0MQmwNvJ+bPeyd8sOtoiNyXp55ZjO4pXewBdFHD0f9PvGovf2m5x/1typA=="
     }
   }
 }
diff --git a/modules/jooby-swagger-ui/package.json b/modules/jooby-swagger-ui/package.json
index 635901990..75c14efb7 100644
--- a/modules/jooby-swagger-ui/package.json
+++ b/modules/jooby-swagger-ui/package.json
@@ -4,6 +4,6 @@
   "private": true,
   "license": "ASF",
   "dependencies": {
-    "swagger-ui-dist": "^3.32.4"
+    "swagger-ui-dist": "^3.32.1"
   }
 }
diff --git a/pom.xml b/pom.xml
index 48c45dcbe..9e18cad94 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,10 +32,10 @@
     <mysql-connector-java.version>8.0.21</mysql-connector-java.version>
     <log4jdbc.version>1.2</log4jdbc.version>
     <h2.version>1.4.200</h2.version>
-    <hibernate.version>5.4.20.Final</hibernate.version>
+    <hibernate.version>5.4.19.Final</hibernate.version>
     <ebean.version>12.3.7</ebean.version>
     <jdbi.version>3.14.1</jdbi.version>
-    <flyway.version>6.5.5</flyway.version>
+    <flyway.version>6.5.4</flyway.version>
     <graphql-java.version>15.0</graphql-java.version>
     <lettuce.version>5.3.2.RELEASE</lettuce.version>
     <caffeine.version>2.8.5</caffeine.version>
@@ -72,7 +72,7 @@
 
     <!-- Reactive -->
     <rxjava.version>2.2.19</rxjava.version>
-    <reactor.version>3.3.9.RELEASE</reactor.version>
+    <reactor.version>3.3.8.RELEASE</reactor.version>
 
     <!--OpenAPI-->
     <swagger.version>2.1.4</swagger.version>
@@ -100,8 +100,8 @@
     <unbescape.version>1.1.6.RELEASE</unbescape.version>
 
     <!-- Kotlin -->
-    <kotlin.version>1.4.0</kotlin.version>
-    <kotlinx-coroutines-core.version>1.3.9</kotlinx-coroutines-core.version>
+    <kotlin.version>1.3.72</kotlin.version>
+    <kotlinx-coroutines-core.version>1.3.7</kotlinx-coroutines-core.version>
 
     <!-- Test dependencies -->
     <jacoco.version>0.8.5</jacoco.version>
@@ -1440,7 +1440,7 @@
               <dependency>
                 <groupId>org.codehaus.groovy</groupId>
                 <artifactId>groovy-all</artifactId>
-                <version>3.0.5</version>
+                <version>3.0.4</version>
                 <type>pom</type>
                 <exclusions>
                   <exclusion>


Test patch:
diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIGeneratorTest.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIGeneratorTest.java
index ce100415f..c26e6c4db 100644
--- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIGeneratorTest.java
+++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIGeneratorTest.java
@@ -653,7 +653,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /complexType/1", route.toString());
-          assertEquals("java.util.List<java.lang.String>", route.getDefaultResponse().getJavaType());
+          assertEquals("java.util.List", route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /complexType/2", route.toString());
@@ -784,7 +784,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /literal/2", route.toString());
-          assertEquals(Integer.class.getName(), route.getDefaultResponse().getJavaType());
+          assertEquals(int.class.getName(), route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /literal/3", route.toString());
@@ -792,7 +792,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /literal/4", route.toString());
-          assertEquals(Boolean.class.getName(), route.getDefaultResponse().getJavaType());
+          assertEquals(boolean.class.getName(), route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /call/1", route.toString());
@@ -820,21 +820,19 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /generic/1", route.toString());
-          assertEquals(Object.class.getName(), route.getDefaultResponse().getJavaType());
+          assertEquals(Integer.class.getName(), route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /generic/2", route.toString());
-          assertEquals(Object.class.getName(), route.getDefaultResponse().getJavaType());
+          assertEquals(Integer.class.getName(), route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /generic/3", route.toString());
-          assertEquals(Object.class.getName(), route.getDefaultResponse().getJavaType());
+          assertEquals(String.class.getName(), route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /generic/4", route.toString());
-//          assertEquals(Callable.class.getName() + "<" + Byte.class.getName() + ">",
-//              route.getDefaultResponse().getJavaType());
-          assertEquals(Callable.class.getName(),
+          assertEquals(Callable.class.getName() + "<" + Byte.class.getName() + ">",
               route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
@@ -843,7 +841,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /localvar/2", route.toString());
-          assertEquals(Integer.class.getName(), route.getDefaultResponse().getJavaType());
+          assertEquals(int.class.getName(), route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /localvar/3", route.toString());
@@ -863,7 +861,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /complexType/3", route.toString());
-          assertEquals("java.util.List",
+          assertEquals("java.util.List<java.util.List<java.lang.String>>",
               route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
@@ -873,8 +871,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /multipleTypes/2", route.toString());
-          assertEquals("examples.ABean", route.getDefaultResponse().getJavaTypes().get(0));
-          assertEquals("examples.BBean", route.getDefaultResponse().getJavaTypes().get(1));
+          assertEquals("examples.Bean", route.getDefaultResponse().getJavaTypes().get(0));
         })
         .next(route -> {
           assertEquals("GET /multipleTypes/3", route.toString());
@@ -886,7 +883,7 @@ public class OpenAPIGeneratorTest {
         })
         .next(route -> {
           assertEquals("GET /array/2", route.toString());
-          assertEquals("java.util.List", route.getDefaultResponse().getJavaType());
+          assertEquals("java.util.List<kt.KtRouteReturnType>", route.getDefaultResponse().getJavaType());
         })
         .next(route -> {
           assertEquals("GET /array/3", route.toString());
diff --git a/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java b/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java
deleted file mode 100644
index 1197ff92b..000000000
--- a/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package issues.i1934;
-
-import io.jooby.Jooby;
-
-public class App1934 extends Jooby {
-  {
-    mvc(new Controller1934());
-  }
-}
diff --git a/modules/jooby-openapi/src/test/java/issues/i1934/Controller1934.java b/modules/jooby-openapi/src/test/java/issues/i1934/Controller1934.java
deleted file mode 100644
index 51f737325..000000000
--- a/modules/jooby-openapi/src/test/java/issues/i1934/Controller1934.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package issues.i1934;
-
-import examples.Person;
-import io.jooby.annotations.ContextParam;
-import io.jooby.annotations.GET;
-import io.jooby.annotations.Path;
-
-@Path("/openapi")
-public class Controller1934 {
-
-  @GET
-  @Path("me")
-  public Person getUserInformation(@ContextParam Person user) {
-    return user;
-  }
-}
diff --git a/modules/jooby-openapi/src/test/java/issues/i1934/Issue1934.java b/modules/jooby-openapi/src/test/java/issues/i1934/Issue1934.java
deleted file mode 100644
index c9eb2da10..000000000
--- a/modules/jooby-openapi/src/test/java/issues/i1934/Issue1934.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package issues.i1934;
-
-import io.jooby.internal.openapi.ResponseExt;
-import io.jooby.openapi.OpenAPITest;
-import io.jooby.openapi.RouteIterator;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-public class Issue1934 {
-
-  @OpenAPITest(value = App1934.class, ignoreArguments = true)
-  public void shouldParseContextParamArg(RouteIterator iterator) {
-    iterator.next(route -> {
-          assertNull(route.getParameters());
-          ResponseExt response = route.getDefaultResponse();
-          assertEquals("200", response.getCode());
-          assertEquals("Success", response.getDescription());
-          assertEquals("examples.Person", response.getJavaType());
-        }).verify();
-  }
-}
diff --git a/modules/jooby-openapi/src/test/kotlin/kt/KtRouteReturnType.kt b/modules/jooby-openapi/src/test/kotlin/kt/KtRouteReturnType.kt
index 8cd34bf6d..b384d357b 100644
--- a/modules/jooby-openapi/src/test/kotlin/kt/KtRouteReturnType.kt
+++ b/modules/jooby-openapi/src/test/kotlin/kt/KtRouteReturnType.kt
@@ -90,9 +90,9 @@ class KtRouteReturnType : Kooby({
       q
     }
     get("/3") {
-      val listOfList: List<List<String>> = ArrayList()
-      listOfList.stream().filter { obj: List<String>? -> Objects.nonNull(obj) }.toArray()
-      listOfList
+      val values: List<List<String>> = ArrayList()
+      values.stream().filter { obj: List<String>? -> Objects.nonNull(obj) }.toArray()
+      values
     }
   }
   path("/multipleTypes") {
@@ -124,8 +124,8 @@ class KtRouteReturnType : Kooby({
   path("/array") {
     get("/1") { booleanArrayOf(true, false, false) }
     get("/2") {
-      val listOfKtRouteReturnType = listOf<KtRouteReturnType>()
-      listOfKtRouteReturnType
+      val list = listOf<KtRouteReturnType>()
+      list
     }
     get("/3") { IntArray(0) }
     get("/4") { arrayOf("foo", "bar") }
@@ -142,4 +142,6 @@ class KtRouteReturnType : Kooby({
       return "static"
     }
   }
+
+
 }
diff --git a/modules/jooby-test/src/main/java/io/jooby/MockContext.java b/modules/jooby-test/src/main/java/io/jooby/MockContext.java
index c5b139e5f..ba00dccb1 100644
--- a/modules/jooby-test/src/main/java/io/jooby/MockContext.java
+++ b/modules/jooby-test/src/main/java/io/jooby/MockContext.java
@@ -604,7 +604,7 @@ public class MockContext implements DefaultContext {
     return this;
   }
 
-  @Nonnull @Override public Context send(@Nonnull FileDownload file) {
+  @Nonnull @Override public Context send(@Nonnull AttachedFile file) {
     responseStarted = true;
     this.response.setResult(file);
     listeners.run(this);
diff --git a/tests/src/test/java/io/jooby/FeaturedTest.java b/tests/src/test/java/io/jooby/FeaturedTest.java
index ce4c9a5d7..0ff609285 100644
--- a/tests/src/test/java/io/jooby/FeaturedTest.java
+++ b/tests/src/test/java/io/jooby/FeaturedTest.java
@@ -1526,18 +1526,6 @@ public class FeaturedTest {
         Path file = userdir("src", "test", "resources", "files", "19kb.txt");
         return new AttachedFile(file, ctx.query("name").value(file.getFileName().toString()));
       });
-      app.get("/inline", ctx -> {
-        Path file = userdir("src", "test", "resources", "files", "19kb.txt");
-        return new InlineFile(file, ctx.query("name").value(file.getFileName().toString()));
-      });
-      app.get("/attachment-builder", ctx -> {
-        Path file = userdir("src", "test", "resources", "files", "19kb.txt");
-        return FileDownload.build(file, ctx.query("name").value(file.getFileName().toString())).attachment();
-      });
-      app.get("/inline-builder", ctx -> {
-        Path file = userdir("src", "test", "resources", "files", "19kb.txt");
-        return FileDownload.build(file, ctx.query("name").value(file.getFileName().toString())).inline();
-      });
     }).ready(client -> {
       client.get("/filechannel", rsp -> {
         assertEquals(null, rsp.header("transfer-encoding"));
@@ -1574,36 +1562,6 @@ public class FeaturedTest {
         assertEquals(_19kb, rsp.body().string());
       });
 
-      client.get("/inline", rsp -> {
-        assertEquals(null, rsp.header("transfer-encoding"));
-        assertEquals(Integer.toString(_19kb.length()),
-            rsp.header("content-length").toLowerCase());
-        assertEquals("text/plain;charset=utf-8", rsp.header("content-type").toLowerCase());
-        assertEquals("inline;filename=\"19kb.txt\"",
-            rsp.header("content-disposition").toLowerCase());
-        assertEquals(_19kb, rsp.body().string());
-      });
-
-      client.get("/attachment-builder", rsp -> {
-        assertEquals(null, rsp.header("transfer-encoding"));
-        assertEquals(Integer.toString(_19kb.length()),
-            rsp.header("content-length").toLowerCase());
-        assertEquals("text/plain;charset=utf-8", rsp.header("content-type").toLowerCase());
-        assertEquals("attachment;filename=\"19kb.txt\"",
-            rsp.header("content-disposition").toLowerCase());
-        assertEquals(_19kb, rsp.body().string());
-      });
-
-      client.get("/inline-builder", rsp -> {
-        assertEquals(null, rsp.header("transfer-encoding"));
-        assertEquals(Integer.toString(_19kb.length()),
-            rsp.header("content-length").toLowerCase());
-        assertEquals("text/plain;charset=utf-8", rsp.header("content-type").toLowerCase());
-        assertEquals("inline;filename=\"19kb.txt\"",
-            rsp.header("content-disposition").toLowerCase());
-        assertEquals(_19kb, rsp.body().string());
-      });
-
       client.get("/attachment?name=foo+bar.txt", rsp -> {
         assertEquals(null, rsp.header("transfer-encoding"));
         assertEquals(Integer.toString(_19kb.length()),