doc #20466 akka http java dsl examples - file upload directives (#20859)

* #20466 file upload - first example test done

* #20466 second test from FileUploadDirectivesExample passing in Java

* #20466 FileUploadDirectives examples file rename plus docs

*  #20466 post-review changes - adding better JAVA Multipart API

* #20466 Multipart Java conversions fix and added spec for the Java side of the model
This commit is contained in:
gosubpl 2016-07-05 10:26:27 +02:00 committed by Konrad Malawski
parent 91eb27947b
commit 06aaa273f1
8 changed files with 387 additions and 9 deletions

View file

@ -0,0 +1,134 @@
/**
* Copyright (C) 2016-2016 Lightbend Inc. <http://www.lightbend.com>
*/
package docs.http.javadsl.server.directives;
import akka.http.impl.engine.rendering.BodyPartRenderer;
import akka.http.javadsl.model.*;
import akka.http.javadsl.server.Route;
import akka.http.javadsl.server.Unmarshaller;
import akka.http.javadsl.server.directives.FileInfo;
import akka.http.javadsl.testkit.JUnitRouteTest;
import akka.stream.javadsl.Framing;
import akka.stream.javadsl.Source;
import akka.util.ByteString;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
public class FileUploadDirectivesExamplesTest extends JUnitRouteTest {
@Test
public void testUploadedFile() {
//#uploadedFile
// function (FileInfo, File) => Route to process the file metadata and file itself
BiFunction<FileInfo, File, Route> infoFileRoute =
(info, file) -> {
// do something with the file and file metadata ...
file.delete();
return complete(StatusCodes.OK);
};
final Route route = uploadedFile("csv", infoFileRoute);
Map<String, String> filenameMapping = new HashMap<>();
filenameMapping.put("filename", "data.csv");
akka.http.javadsl.model.Multipart.FormData multipartForm =
Multiparts.createStrictFormDataFromParts(Multiparts.createFormDataBodyPartStrict("csv",
HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8,
"1,5,7\n11,13,17"), filenameMapping));
// test:
testRoute(route).run(HttpRequest.POST("/").withEntity(
multipartForm.toEntity(HttpCharsets.UTF_8, BodyPartRenderer
.randomBoundaryWithDefaults())))
.assertStatusCode(StatusCodes.OK);
//#
}
@Test
public void testFileUpload() {
//#fileUpload
final Route route = extractRequestContext(ctx -> {
// function (FileInfo, Source<ByteString,Object>) => Route to process the file contents
BiFunction<FileInfo, Source<ByteString, Object>, Route> processUploadedFile =
(metadata, byteSource) -> {
CompletionStage<Integer> sumF = byteSource.via(Framing.delimiter(
ByteString.fromString("\n"), 1024))
.mapConcat(bs -> Arrays.asList(bs.utf8String().split(",")))
.map(s -> Integer.parseInt(s))
.runFold(0, (acc, n) -> acc + n, ctx.getMaterializer());
return onSuccess(() -> sumF, sum -> complete("Sum: " + sum));
};
return fileUpload("csv", processUploadedFile);
});
Map<String, String> filenameMapping = new HashMap<>();
filenameMapping.put("filename", "primes.csv");
akka.http.javadsl.model.Multipart.FormData multipartForm =
Multiparts.createStrictFormDataFromParts(
Multiparts.createFormDataBodyPartStrict("csv",
HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8,
"2,3,5\n7,11,13,17,23\n29,31,37\n"), filenameMapping));
// test:
testRoute(route).run(HttpRequest.POST("/").withEntity(
multipartForm.toEntity(HttpCharsets.UTF_8, BodyPartRenderer.randomBoundaryWithDefaults())))
.assertStatusCode(StatusCodes.OK).assertEntityAs(Unmarshaller.entityToString(), "Sum: 178");
//#
}
@Ignore("compileOnly")
@Test
public void testFileProcessing() {
//#fileProcessing
final Route route = extractRequestContext(ctx -> {
// function (FileInfo, Source<ByteString,Object>) => Route to process the file contents
BiFunction<FileInfo, Source<ByteString, Object>, Route> processUploadedFile =
(metadata, byteSource) -> {
CompletionStage<Integer> sumF = byteSource.via(Framing.delimiter(
ByteString.fromString("\n"), 1024))
.mapConcat(bs -> Arrays.asList(bs.utf8String().split(",")))
.map(s -> Integer.parseInt(s))
.runFold(0, (acc, n) -> acc + n, ctx.getMaterializer());
return onSuccess(() -> sumF, sum -> complete("Sum: " + sum));
};
return fileUpload("csv", processUploadedFile);
});
Map<String, String> filenameMapping = new HashMap<>();
filenameMapping.put("filename", "primes.csv");
String prefix = "primes";
String suffix = ".csv";
File tempFile = null;
try {
tempFile = File.createTempFile(prefix, suffix);
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), Arrays.asList("2,3,5", "7,11,13,17,23", "29,31,37"), Charset.forName("UTF-8"));
} catch (Exception e) {
// ignore
}
akka.http.javadsl.model.Multipart.FormData multipartForm =
Multiparts.createFormDataFromPath("csv", ContentTypes.TEXT_PLAIN_UTF8, tempFile.toPath());
// test:
testRoute(route).run(HttpRequest.POST("/").withEntity(
multipartForm.toEntity(HttpCharsets.UTF_8, BodyPartRenderer.randomBoundaryWithDefaults())))
.assertStatusCode(StatusCodes.OK).assertEntityAs(Unmarshaller.entityToString(), "Sum: 178");
//#
}
}

View file

@ -14,7 +14,8 @@ with the same name, the first one will be used and the subsequent ones ignored.
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode2:: ../../../../code/docs/http/javadsl/server/directives/FileUploadDirectivesExamplesTest.java
:snippet: fileUpload
::

View file

@ -20,4 +20,5 @@ one will be used and the subsequent ones ignored.
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode2:: ../../../../code/docs/http/javadsl/server/directives/FileUploadDirectivesExamplesTest.java
:snippet: uploadedFile

View file

@ -87,7 +87,7 @@ public final class HttpEntities {
(akka.http.scaladsl.model.ContentType) contentType,
toScala(data));
}
private static akka.stream.scaladsl.Source<ByteString,Object> toScala(Source<ByteString, ?> javaSource) {
return (akka.stream.scaladsl.Source<ByteString,Object>)javaSource.asScala();
}

View file

@ -0,0 +1,140 @@
/*
* Copyright (C) 2016-2016 Lightbend Inc. <http://www.lightbend.com>
*/
package akka.http.javadsl.model;
import scala.collection.immutable.List;
import scala.collection.immutable.Nil$;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import static akka.http.impl.util.Util.convertArray;
import static akka.http.impl.util.Util.convertMapToScala;
import static akka.http.impl.util.Util.emptyMap;
/**
* Constructors for Multipart instances
*/
public final class Multiparts {
/**
* Constructor for `multipart/form-data` content as defined in http://tools.ietf.org/html/rfc2388.
* All parts must have distinct names. (This is not verified!)
*/
public static Multipart.FormData createFormDataFromParts(Multipart.FormData.BodyPart... parts) {
return akka.http.scaladsl.model.Multipart.FormData$.MODULE$.createNonStrict(convertArray(parts));
}
/**
* Constructor for `multipart/form-data` content as defined in http://tools.ietf.org/html/rfc2388.
* All parts must have distinct names. (This is not verified!)
*/
public static Multipart.FormData.Strict createStrictFormDataFromParts(Multipart.FormData.BodyPart.Strict... parts) {
return akka.http.scaladsl.model.Multipart.FormData$.MODULE$.createStrict(convertArray(parts));
}
/**
* Constructor for `multipart/form-data` content as defined in http://tools.ietf.org/html/rfc2388.
* All parts must have distinct names. (This is not verified!)
*/
public static Multipart.FormData.Strict createFormDataFromFields(Map<String, HttpEntity.Strict> fields) {
return akka.http.scaladsl.model.Multipart.FormData$.MODULE$.createStrict(toScalaMap(fields));
}
/**
* Creates a FormData instance that contains a single part backed by the given file.
*
* To create an instance with several parts or for multiple files, use
* `Multiparts.createFormDataFromParts(Multiparts.createFormDataPartFromPath("field1", ...), Multiparts.createFormDataPartFromPath("field2", ...)`
*/
public static Multipart.FormData createFormDataFromPath(String name, ContentType contentType, Path path, int chunkSize) {
return akka.http.scaladsl.model.Multipart.FormData$.MODULE$.fromPath(name, (akka.http.scaladsl.model.ContentType) contentType, path, chunkSize);
}
/**
* Creates a FormData instance that contains a single part backed by the given file.
*
* To create an instance with several parts or for multiple files, use
* `Multiparts.createFormDataFromParts(Multiparts.createFormDataPartFromPath("field1", ...), Multiparts.createFormDataPartFromPath("field2", ...)`
*/
public static Multipart.FormData createFormDataFromPath(String name, ContentType contentType, Path path) {
return akka.http.scaladsl.model.Multipart.FormData$.MODULE$.fromPath(name, (akka.http.scaladsl.model.ContentType) contentType, path, -1);
}
/**
* Creates a BodyPart backed by a file that will be streamed using a FileSource.
*/
public static Multipart.FormData.BodyPart createFormDataPartFromPath(String name, ContentType contentType, Path path, int chunkSize) {
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$.MODULE$.fromPath(name, (akka.http.scaladsl.model.ContentType) contentType, path, chunkSize);
}
/**
* Creates a BodyPart backed by a file that will be streamed using a FileSource.
*/
public static Multipart.FormData.BodyPart createFormDataPartFromPath(String name, ContentType contentType, Path path) {
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$.MODULE$.fromPath(name, (akka.http.scaladsl.model.ContentType) contentType, path, -1);
}
/**
* Creates a BodyPart.
*/
public static Multipart.FormData.BodyPart createFormDataBodyPart(String name, BodyPartEntity entity) {
List nil = Nil$.MODULE$;
Map<String, String> additionalDispositionParams = Collections.emptyMap();
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$Builder$.MODULE$.create(name, (akka.http.scaladsl.model.BodyPartEntity) entity,
convertMapToScala(additionalDispositionParams), nil);
}
/**
* Creates a BodyPart.
*/
public static Multipart.FormData.BodyPart createFormDataBodyPart(String name, BodyPartEntity entity, Map<String, String> additionalDispositionParams) {
List nil = Nil$.MODULE$;
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$Builder$.MODULE$.create(name, (akka.http.scaladsl.model.BodyPartEntity) entity,
convertMapToScala(additionalDispositionParams), nil);
}
/**
* Creates a BodyPart.
*/
public static Multipart.FormData.BodyPart createFormDataBodyPart(String name, BodyPartEntity entity, Map<String, String> additionalDispositionParams, java.util.List<HttpHeader> headers) {
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$Builder$.MODULE$.create(name, (akka.http.scaladsl.model.BodyPartEntity) entity,
convertMapToScala(additionalDispositionParams), toScalaSeq(headers));
}
/**
* Creates a BodyPart.Strict.
*/
public static Multipart.FormData.BodyPart.Strict createFormDataBodyPartStrict(String name, HttpEntity.Strict entity) {
List nil = Nil$.MODULE$;
Map<String, String> additionalDispositionParams = Collections.emptyMap();
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$StrictBuilder$.MODULE$.createStrict(name, (akka.http.scaladsl.model.HttpEntity.Strict) entity,
convertMapToScala(additionalDispositionParams), nil);
}
/**
* Creates a BodyPart.Strict.
*/
public static Multipart.FormData.BodyPart.Strict createFormDataBodyPartStrict(String name, HttpEntity.Strict entity, Map<String, String> additionalDispositionParams) {
List nil = Nil$.MODULE$;
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$StrictBuilder$.MODULE$.createStrict(name, (akka.http.scaladsl.model.HttpEntity.Strict) entity,
convertMapToScala(additionalDispositionParams), nil);
}
/**
* Creates a BodyPart.Strict.
*/
public static Multipart.FormData.BodyPart.Strict createFormDataBodyPartStrict(String name, HttpEntity.Strict entity, Map<String, String> additionalDispositionParams, java.util.List<HttpHeader> headers) {
return akka.http.scaladsl.model.Multipart$FormData$BodyPart$StrictBuilder$.MODULE$.createStrict(name, (akka.http.scaladsl.model.HttpEntity.Strict) entity,
convertMapToScala(additionalDispositionParams), toScalaSeq(headers));
}
private static scala.collection.immutable.Map<String, HttpEntity.Strict> toScalaMap(Map<String, HttpEntity.Strict> map) {
return emptyMap.$plus$plus(scala.collection.JavaConverters.mapAsScalaMapConverter(map).asScala());
}
private static scala.collection.Iterable<HttpHeader> toScalaSeq(java.util.List<HttpHeader> _headers) {
return scala.collection.JavaConverters.collectionAsScalaIterableConverter(_headers).asScala();
}
}

View file

@ -139,4 +139,14 @@ private[http] object BodyPartRenderer {
random.nextBytes(array)
Base64.custom.encodeToString(array, false)
}
/**
* Creates a new random number of default length and base64 encodes it (using a custom "safe" alphabet).
*/
def randomBoundaryWithDefaults(): String = randomBoundary()
/**
* Creates a new random number of the given length and base64 encodes it (using a custom "safe" alphabet).
*/
def randomBoundaryWithDefaultRandom(length: Int): String = randomBoundary(length)
}

View file

@ -212,7 +212,7 @@ object Multipart {
/** Java API */
override def toStrict(timeoutMillis: Long, materializer: Materializer): CompletionStage[jm.Multipart.General.Strict] =
super.toStrict(timeoutMillis, materializer).asInstanceOf[Future[jm.Multipart.General.Strict]].toJava
super.toStrict(timeoutMillis, materializer).toScala.asInstanceOf[Future[jm.Multipart.General.Strict]].toJava
}
object General {
def apply(mediaType: MediaType.Multipart, parts: BodyPart.Strict*): Strict = Strict(mediaType, parts.toVector)
@ -258,7 +258,7 @@ object Multipart {
/** Java API */
override def toStrict(timeoutMillis: Long, materializer: Materializer): CompletionStage[jm.Multipart.General.BodyPart.Strict] =
super.toStrict(timeoutMillis, materializer).asInstanceOf[Future[jm.Multipart.General.BodyPart.Strict]].toJava
super.toStrict(timeoutMillis, materializer).toScala.asInstanceOf[Future[jm.Multipart.General.BodyPart.Strict]].toJava
private[BodyPart] def tryCreateFormDataBodyPart[T](f: (String, Map[String, String], immutable.Seq[HttpHeader]) T): Try[T] = {
val params = dispositionParams
@ -323,12 +323,22 @@ object Multipart {
/** Java API */
override def toStrict(timeoutMillis: Long, materializer: Materializer): CompletionStage[jm.Multipart.FormData.Strict] =
super.toStrict(timeoutMillis, materializer).asInstanceOf[Future[jm.Multipart.FormData.Strict]].toJava
super.toStrict(timeoutMillis, materializer).toScala.asInstanceOf[Future[jm.Multipart.FormData.Strict]].toJava
}
object FormData {
def apply(parts: Multipart.FormData.BodyPart.Strict*): Multipart.FormData.Strict = Strict(parts.toVector)
def apply(parts: Multipart.FormData.BodyPart*): Multipart.FormData = Multipart.FormData(Source(parts.toVector))
// FIXME: SI-2991 workaround - two functions below. Remove when (hopefully) this issue is fixed
/** INTERNAL API */
private[akka] def createStrict(parts: Multipart.FormData.BodyPart.Strict*): Multipart.FormData.Strict = Strict(parts.toVector)
/** INTERNAL API */
private[akka] def createNonStrict(parts: Multipart.FormData.BodyPart*): Multipart.FormData = Multipart.FormData(Source(parts.toVector))
/** INTERNAL API */
private[akka] def createStrict(fields: Map[String, akka.http.javadsl.model.HttpEntity.Strict]): Multipart.FormData.Strict = Multipart.FormData.Strict {
fields.map { case (name, entity: akka.http.scaladsl.model.HttpEntity.Strict) Multipart.FormData.BodyPart.Strict(name, entity) }(collection.breakOut)
}
def apply(fields: Map[String, HttpEntity.Strict]): Multipart.FormData.Strict = Multipart.FormData.Strict {
fields.map { case (name, entity) Multipart.FormData.BodyPart.Strict(name, entity) }(collection.breakOut)
}
@ -426,7 +436,7 @@ object Multipart {
/** Java API */
override def toStrict(timeoutMillis: Long, materializer: Materializer): CompletionStage[jm.Multipart.FormData.BodyPart.Strict] =
super.toStrict(timeoutMillis, materializer).asInstanceOf[Future[jm.Multipart.FormData.BodyPart.Strict]].toJava
super.toStrict(timeoutMillis, materializer).toScala.asInstanceOf[Future[jm.Multipart.FormData.BodyPart.Strict]].toJava
}
object BodyPart {
def apply(_name: String, _entity: BodyPartEntity,
@ -467,6 +477,26 @@ object Multipart {
FastFuture.successful(this)
override def productPrefix = "FormData.BodyPart.Strict"
}
/** INTERNAL API */
private[akka] object Builder {
def create(_name: String, _entity: BodyPartEntity,
_additionalDispositionParams: Map[String, String],
_additionalHeaders: Iterable[akka.http.javadsl.model.HttpHeader]): Multipart.FormData.BodyPart = {
val _headers = _additionalHeaders.to[immutable.Seq] map { case h: akka.http.scaladsl.model.HttpHeader h }
apply(_name, _entity, _additionalDispositionParams, _headers)
}
}
/** INTERNAL API */
private[akka] object StrictBuilder {
def createStrict(_name: String, _entity: HttpEntity.Strict,
_additionalDispositionParams: Map[String, String],
_additionalHeaders: Iterable[akka.http.javadsl.model.HttpHeader]): Multipart.FormData.BodyPart.Strict = {
val _headers = _additionalHeaders.to[immutable.Seq] map { case h: akka.http.scaladsl.model.HttpHeader h }
Strict(_name, _entity, _additionalDispositionParams, _headers)
}
}
}
}
@ -488,7 +518,7 @@ object Multipart {
/** Java API */
override def toStrict(timeoutMillis: Long, materializer: Materializer): CompletionStage[jm.Multipart.ByteRanges.Strict] =
super.toStrict(timeoutMillis, materializer).asInstanceOf[Future[jm.Multipart.ByteRanges.Strict]].toJava
super.toStrict(timeoutMillis, materializer).toScala.asInstanceOf[Future[jm.Multipart.ByteRanges.Strict]].toJava
}
object ByteRanges {
def apply(parts: Multipart.ByteRanges.BodyPart.Strict*): Strict = Strict(parts.toVector)
@ -563,7 +593,7 @@ object Multipart {
/** Java API */
override def toStrict(timeoutMillis: Long, materializer: Materializer): CompletionStage[jm.Multipart.ByteRanges.BodyPart.Strict] =
super.toStrict(timeoutMillis, materializer).asInstanceOf[Future[jm.Multipart.ByteRanges.BodyPart.Strict]].toJava
super.toStrict(timeoutMillis, materializer).toScala.asInstanceOf[Future[jm.Multipart.ByteRanges.BodyPart.Strict]].toJava
}
object BodyPart {
def apply(_contentRange: ContentRange, _entity: BodyPartEntity, _rangeUnit: RangeUnit = RangeUnits.Bytes,

View file

@ -0,0 +1,62 @@
/*
* Copyright (C) 2016-2016 Lightbend Inc. <http://www.lightbend.com>
*/
package akka.http.javadsl.model
import java.util
import com.typesafe.config.{ Config, ConfigFactory }
import scala.concurrent.Await
import scala.concurrent.duration._
import org.scalatest.{ BeforeAndAfterAll, Inside, Matchers, WordSpec }
import akka.stream.ActorMaterializer
import akka.actor.ActorSystem
import scala.compat.java8.FutureConverters
class MultipartsSpec extends WordSpec with Matchers with Inside with BeforeAndAfterAll {
val testConf: Config = ConfigFactory.parseString("""
akka.event-handlers = ["akka.testkit.TestEventListener"]
akka.loglevel = WARNING""")
implicit val system = ActorSystem(getClass.getSimpleName, testConf)
implicit val materializer = ActorMaterializer()
override def afterAll() = system.terminate()
"Multiparts.createFormDataFromParts" should {
"create a model from Multiparts.createFormDataBodyPartparts" in {
val streamed = Multiparts.createFormDataFromParts(
Multiparts.createFormDataBodyPart("foo", HttpEntities.create("FOO")),
Multiparts.createFormDataBodyPart("bar", HttpEntities.create("BAR")))
val strictCS = streamed.toStrict(1000, materializer)
val strict = Await.result(FutureConverters.toScala(strictCS), 1.second)
strict shouldEqual akka.http.scaladsl.model.Multipart.FormData(
Map("foo" akka.http.scaladsl.model.HttpEntity("FOO"), "bar" akka.http.scaladsl.model.HttpEntity("BAR")))
}
}
"Multiparts.createFormDataFromFields" should {
"create a model from a map of fields" in {
val fields = new util.HashMap[String, HttpEntity.Strict]
fields.put("foo", HttpEntities.create("FOO"))
val streamed = Multiparts.createFormDataFromFields(fields)
val strictCS = streamed.toStrict(1000, materializer)
val strict = Await.result(FutureConverters.toScala(strictCS), 1.second)
strict shouldEqual akka.http.scaladsl.model.Multipart.FormData(
Map("foo" akka.http.scaladsl.model.HttpEntity("FOO")))
}
}
"Multiparts.createStrictFormDataFromParts" should {
"create a strict model from Multiparts.createFormDataBodyPartStrict parts" in {
val streamed = Multiparts.createStrictFormDataFromParts(
Multiparts.createFormDataBodyPartStrict("foo", HttpEntities.create("FOO")),
Multiparts.createFormDataBodyPartStrict("bar", HttpEntities.create("BAR")))
val strict = streamed
strict shouldEqual akka.http.scaladsl.model.Multipart.FormData(
Map("foo" akka.http.scaladsl.model.HttpEntity("FOO"), "bar" akka.http.scaladsl.model.HttpEntity("BAR")))
}
}
}