diff --git a/play-java-fileupload-example/.gitignore b/play-java-fileupload-example/.gitignore new file mode 100644 index 000000000..0e21b3a46 --- /dev/null +++ b/play-java-fileupload-example/.gitignore @@ -0,0 +1,10 @@ +logs +target +build +/.idea +/.idea_modules +/.classpath +/.gradle +/.project +/.settings +/RUNNING_PID diff --git a/play-java-fileupload-example/.mergify.yml b/play-java-fileupload-example/.mergify.yml new file mode 100644 index 000000000..32f8689ae --- /dev/null +++ b/play-java-fileupload-example/.mergify.yml @@ -0,0 +1,27 @@ +pull_request_rules: + - name: automatic merge on CI success require review + conditions: + - status-success=Travis CI - Pull Request + - "#approved-reviews-by>=1" + - "#changes-requested-reviews-by=0" + - label!=block-merge + actions: + merge: + method: squash + strict: smart + + - name: automatic merge on CI success for TemplateControl + conditions: + - status-success=Travis CI - Pull Request + - label=merge-when-green + - label!=block-merge + actions: + merge: + method: squash + strict: smart + + - name: delete branch after merge + conditions: + - merged + actions: + delete_head_branch: {} diff --git a/play-java-fileupload-example/.travis.yml b/play-java-fileupload-example/.travis.yml new file mode 100644 index 000000000..1e8c0e7c4 --- /dev/null +++ b/play-java-fileupload-example/.travis.yml @@ -0,0 +1,50 @@ +language: scala +scala: + - 2.12.8 + +before_install: + - curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | bash && . ~/.jabba/jabba.sh + +env: + global: + - JABBA_HOME=$HOME/.jabba + matrix: + # There is no concise way to specify multi-dimensional build matrix: + # https://github.com/travis-ci/travis-ci/issues/1519 + - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.8.192-12 + - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-1 + - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.192-12 + - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-1 + +# Exclude some combinations from build matrix. See: +# https://docs.travis-ci.com/user/customizing-the-build/#Build-Matrix +matrix: + fast_finish: true + allow_failures: + # Current release of Gradle still does not supports Play 2.7.x releases + # As soon as there is a release of Gradle that fixes that, we can then + # remove this allowed failure. + - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.192-12 + - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-1 + # Java 11 is still not fully supported. It is good that we are already + # testing our sample applications to better discover possible problems + # but we can allow failures here too. + - env: SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-1 + +install: + - $JABBA_HOME/bin/jabba install $TRAVIS_JDK + - unset _JAVA_OPTIONS + - export JAVA_HOME="$JABBA_HOME/jdk/$TRAVIS_JDK" && export PATH="$JAVA_HOME/bin:$PATH" && java -Xmx32m -version + +script: + - $SCRIPT + +before_cache: + - find $HOME/.ivy2 -name "ivydata-*.properties" -delete + - find $HOME/.sbt -name "*.lock" -delete + +cache: + directories: + - "$HOME/.ivy2/cache" + - "$HOME/.gradle/caches" + - "$HOME/.jabba/jdk" diff --git a/play-java-fileupload-example/LICENSE b/play-java-fileupload-example/LICENSE new file mode 100644 index 000000000..670154e35 --- /dev/null +++ b/play-java-fileupload-example/LICENSE @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + diff --git a/play-java-fileupload-example/NOTICE b/play-java-fileupload-example/NOTICE new file mode 100644 index 000000000..6d6c034d3 --- /dev/null +++ b/play-java-fileupload-example/NOTICE @@ -0,0 +1,8 @@ +Written by Lightbend + +To the extent possible under law, the author(s) have dedicated all copyright and +related and neighboring rights to this software to the public domain worldwide. +This software is distributed without any warranty. + +You should have received a copy of the CC0 Public Domain Dedication along with +this software. If not, see . diff --git a/play-java-fileupload-example/README.md b/play-java-fileupload-example/README.md new file mode 100644 index 000000000..71909a34a --- /dev/null +++ b/play-java-fileupload-example/README.md @@ -0,0 +1,96 @@ +# Play File Upload using a custom BodyParser + +[![Build Status](https://travis-ci.org/playframework/play-java-fileupload-example.svg?branch=2.6.x)](https://travis-ci.org/playframework/play-java-fileupload-example) + +This is a sample project that shows how to upload a file through Akka Streams using a custom BodyParser using Akka Streams using the Java API. + +## Default MultipartFormData Body Parser + +Play's Java API specifies a BodyParser.MultipartFormData class which uses a TemporaryFile wrapper class that creates a file under a "temporary" name and then deletes it only when the system is under GC pressure. + +```java +@BodyParser.Of(BodyParser.MultipartFormData.class) +public Result upload() throws IOException { + final Http.MultipartFormData formData = request().body().asMultipartFormData(); + final Http.MultipartFormData.FilePart filePart = formData.getFile("name"); + final File file = filePart.getFile(); + final long data = operateOnTempFile(file); + return ok("file size = " + data + ""); +} +``` + +## Customizing the Body Parser + +There are cases where it's useful to have more control over where and Play uploads multi part form data. In this case, we'd like to get access to the accumulated byte stream for each file part and generate a file directly, without going through TemporaryFile. + +In short, we want to replace: + +```java +@BodyParser.Of(BodyParser.MultipartFormData.class) +``` + +with: + +```java +@BodyParser.Of(MyMultipartFormDataBodyParser.class) +``` + +And we want to change as little code as possible. The underlying mechanics are simple. `MyMultipartFormDataBodyParser` does all the work of setting up a custom file part handler using a method called `createFilePartHandler`: + +```java +class MyMultipartFormDataBodyParser extends DelegatingMultipartFormDataBodyParser { + + @Inject + public MyMultipartFormDataBodyParser(Materializer materializer, play.api.http.HttpConfiguration config) { + super(materializer, config.parser().maxDiskBuffer()); + } + + /** + * Creates a file part handler that uses a custom accumulator. + */ + @Override + public Function>> createFilePartHandler() { + return (Multipart.FileInfo fileInfo) -> { + final String filename = fileInfo.fileName(); + final String partname = fileInfo.partName(); + final String contentType = fileInfo.contentType().getOrElse(null); + final File file = generateTempFile(); + + final Sink> sink = FileIO.toFile(file); + return Accumulator.fromSink( + sink.mapMaterializedValue(completionStage -> + completionStage.thenApplyAsync(results -> { + //noinspection unchecked + return new Http.MultipartFormData.FilePart(partname, + filename, + contentType, + file); + }) + )); + }; + } + + /** + * Generates a temp file directly without going through TemporaryFile. + */ + private File generateTempFile() { + try { + final Path path = Files.createTempFile("multipartBody", "tempFile"); + return path.toFile(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + +} +``` + +The core Accumulator is generated from an `akka.streams.FileIO` sink which writes out bytes to the filesystem, and exposes a CompletionStage when the write operation has been completed. + +Because this code delegates to the Scala API implementation, the underlying `DelegatingMultipartFormDataBodyParser` exposes an abstract method: + +```java +abstract Function>> createFilePartHandler(); +``` + +`DelegatingMultipartFormDataBodyParser` does not know about any particular type, only `FilePart`, and so it falls to the implementation to fill in the details. diff --git a/play-java-fileupload-example/app/controllers/FormData.java b/play-java-fileupload-example/app/controllers/FormData.java new file mode 100644 index 000000000..3dd6f04b3 --- /dev/null +++ b/play-java-fileupload-example/app/controllers/FormData.java @@ -0,0 +1,17 @@ +package controllers; + +public class FormData { + private String name; + + public FormData() {} + + public FormData(String name) { + this.name = name; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } +} diff --git a/play-java-fileupload-example/app/controllers/HomeController.java b/play-java-fileupload-example/app/controllers/HomeController.java new file mode 100644 index 000000000..af6c82317 --- /dev/null +++ b/play-java-fileupload-example/app/controllers/HomeController.java @@ -0,0 +1,50 @@ +package controllers; + +import play.data.Form; +import play.mvc.*; +import views.html.index; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +/** + * This class uses a custom body parser to change the upload type. + */ +@Singleton +public class HomeController extends Controller { + + private final play.data.FormFactory formFactory; + + @Inject + public HomeController(play.data.FormFactory formFactory) { + this.formFactory = formFactory; + } + + public Result index() { + Form form = formFactory.form(FormData.class); + return ok(index.render(form)); + } + + /** + * This method uses MyMultipartFormDataBodyParser as the body parser + */ + @BodyParser.Of(MyMultipartFormDataBodyParser.class) + public Result upload() throws IOException { + final Http.MultipartFormData formData = request().body().asMultipartFormData(); + final Http.MultipartFormData.FilePart filePart = formData.getFile("name"); + final File file = filePart.getFile(); + final long data = operateOnTempFile(file); + return ok("file size = " + data + ""); + } + + private long operateOnTempFile(File file) throws IOException { + final long size = Files.size(file.toPath()); + Files.deleteIfExists(file.toPath()); + return size; + } + +} + diff --git a/play-java-fileupload-example/app/controllers/MyMultipartFormDataBodyParser.java b/play-java-fileupload-example/app/controllers/MyMultipartFormDataBodyParser.java new file mode 100644 index 000000000..2cd857601 --- /dev/null +++ b/play-java-fileupload-example/app/controllers/MyMultipartFormDataBodyParser.java @@ -0,0 +1,74 @@ +package controllers; + +import akka.stream.IOResult; +import akka.stream.Materializer; +import akka.stream.javadsl.FileIO; +import akka.stream.javadsl.Sink; +import akka.util.ByteString; +import play.api.http.HttpErrorHandler; +import play.core.parsers.Multipart; +import play.libs.streams.Accumulator; +import play.mvc.BodyParser; +import play.mvc.Http; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +/** + * This class is a custom body parser with a custom file part handler + * that uses a file that can come from anywhere in the system. + */ +class MyMultipartFormDataBodyParser extends BodyParser.DelegatingMultipartFormDataBodyParser { + + @Inject + public MyMultipartFormDataBodyParser(Materializer materializer, play.api.http.HttpConfiguration config, HttpErrorHandler errorHandler) { + super(materializer, config.parser().maxDiskBuffer(), errorHandler); + } + + /** + * Creates a file part handler that uses a custom accumulator. + */ + @Override + public Function>> createFilePartHandler() { + return this::apply; + } + + /** + * Generates a temp file directly without going through TemporaryFile. + */ + private File generateTempFile() { + try { + final Path path = Files.createTempFile("multipartBody", "tempFile"); + return path.toFile(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private Accumulator> apply(Multipart.FileInfo fileInfo) { + final String filename = fileInfo.fileName(); + final String partname = fileInfo.partName(); + final String contentType = fileInfo.contentType().getOrElse(null); + final File file = generateTempFile(); + + final Sink> sink = FileIO.toFile(file); + return Accumulator.fromSink( + sink.mapMaterializedValue(completionStage -> + completionStage.thenApplyAsync(results -> { + //noinspection unchecked + return new Http.MultipartFormData.FilePart<>(partname, + filename, + contentType, + file); + }) + )); + } +} + + + diff --git a/play-java-fileupload-example/app/views/index.scala.html b/play-java-fileupload-example/app/views/index.scala.html new file mode 100644 index 000000000..e7daff336 --- /dev/null +++ b/play-java-fileupload-example/app/views/index.scala.html @@ -0,0 +1,10 @@ +@(form: Form[controllers.FormData]) + +@main("Welcome to Play") { + + @helper.form(action = routes.HomeController.upload, 'enctype -> "multipart/form-data") { + @helper.inputFile(form("name")) + @helper.CSRF.formField + + } +} diff --git a/play-java-fileupload-example/app/views/main.scala.html b/play-java-fileupload-example/app/views/main.scala.html new file mode 100644 index 000000000..9414f4be6 --- /dev/null +++ b/play-java-fileupload-example/app/views/main.scala.html @@ -0,0 +1,23 @@ +@* + * This template is called from the `index` template. This template + * handles the rendering of the page header and body tags. It takes + * two arguments, a `String` for the title of the page and an `Html` + * object to insert into the body of the page. + *@ +@(title: String)(content: Html) + + + + + @* Here's where we render the page title `String`. *@ + @title + + + + + + @* And here's where we render the `Html` object containing + * the page content. *@ + @content + + diff --git a/play-java-fileupload-example/build.gradle b/play-java-fileupload-example/build.gradle new file mode 100644 index 000000000..c54e7124d --- /dev/null +++ b/play-java-fileupload-example/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'play' + id 'idea' +} + +def playVersion = "2.6.21" +def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") + +model { + components { + play { + platform play: playVersion, scala: scalaVersion, java: '1.8' + injectedRoutesGenerator = true + + sources { + twirlTemplates { + defaultImports = TwirlImports.JAVA + } + } + } + } +} + +dependencies { + play "com.typesafe.play:play-guice_$scalaVersion:$playVersion" + play "com.typesafe.play:play-logback_$scalaVersion:$playVersion" + play "com.typesafe.play:filters-helpers_$scalaVersion:$playVersion" +} + +repositories { + jcenter() + maven { + name "lightbend-maven-releases" + url "https://repo.lightbend.com/lightbend/maven-release" + } + ivy { + name "lightbend-ivy-release" + url "https://repo.lightbend.com/lightbend/ivy-releases" + layout "ivy" + } +} diff --git a/play-java-fileupload-example/build.sbt b/play-java-fileupload-example/build.sbt new file mode 100644 index 000000000..97685e900 --- /dev/null +++ b/play-java-fileupload-example/build.sbt @@ -0,0 +1,13 @@ +name := """play-java-fileupload-example""" + +version := "1.0-SNAPSHOT" + +lazy val root = (project in file(".")).enablePlugins(PlayJava) + +scalaVersion := "2.12.8" + +crossScalaVersions := Seq("2.11.12", "2.12.4") + +libraryDependencies += guice + +testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v") diff --git a/play-java-fileupload-example/conf/application.conf b/play-java-fileupload-example/conf/application.conf new file mode 100644 index 000000000..b0023095d --- /dev/null +++ b/play-java-fileupload-example/conf/application.conf @@ -0,0 +1,5 @@ +# https://www.playframework.com/documentation/latest/Configuration + +# Sets the maximum file size that can be uploaded to 1024k. +# https://www.playframework.com/documentation/2.6.x/JavaBodyParsers#Content-length-limits +play.http.parser.maxMemoryBuffer=1024k diff --git a/play-java-fileupload-example/conf/logback.xml b/play-java-fileupload-example/conf/logback.xml new file mode 100644 index 000000000..76222f5d4 --- /dev/null +++ b/play-java-fileupload-example/conf/logback.xml @@ -0,0 +1,44 @@ + + + + + + + ${application.home:-.}/logs/application.log + + %date [%level] from %logger in %thread - %message%n%xException + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-java-fileupload-example/conf/routes b/play-java-fileupload-example/conf/routes new file mode 100644 index 000000000..7cb62e6ad --- /dev/null +++ b/play-java-fileupload-example/conf/routes @@ -0,0 +1,11 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +# An example controller showing a sample home page +GET / controllers.HomeController.index + +POST /upload controllers.HomeController.upload + +# Map static resources from the /public folder to the /assets URL path +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/play-java-fileupload-example/gradle/wrapper/gradle-wrapper.jar b/play-java-fileupload-example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..01b8bf6b1 Binary files /dev/null and b/play-java-fileupload-example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/play-java-fileupload-example/gradle/wrapper/gradle-wrapper.properties b/play-java-fileupload-example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..89dba2d9d --- /dev/null +++ b/play-java-fileupload-example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/play-java-fileupload-example/gradlew b/play-java-fileupload-example/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/play-java-fileupload-example/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/play-java-fileupload-example/gradlew.bat b/play-java-fileupload-example/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/play-java-fileupload-example/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/play-java-fileupload-example/project/build.properties b/play-java-fileupload-example/project/build.properties new file mode 100644 index 000000000..c0bab0494 --- /dev/null +++ b/play-java-fileupload-example/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/play-java-fileupload-example/project/plugins.sbt b/play-java-fileupload-example/project/plugins.sbt new file mode 100644 index 000000000..08d6fe8fe --- /dev/null +++ b/play-java-fileupload-example/project/plugins.sbt @@ -0,0 +1,8 @@ +// The Play plugin +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.21") + +// Play enhancer - this automatically generates getters/setters for public fields +// and rewrites accessors of these fields to use the getters/setters. Remove this +// plugin if you prefer not to have this feature, or disable on a per project +// basis using disablePlugins(PlayEnhancer) in your build.sbt +addSbtPlugin("com.typesafe.sbt" % "sbt-play-enhancer" % "1.2.2") diff --git a/play-java-fileupload-example/public/images/favicon.png b/play-java-fileupload-example/public/images/favicon.png new file mode 100644 index 000000000..c7d92d2ae Binary files /dev/null and b/play-java-fileupload-example/public/images/favicon.png differ diff --git a/play-java-fileupload-example/public/javascripts/hello.js b/play-java-fileupload-example/public/javascripts/hello.js new file mode 100644 index 000000000..02ee13c7c --- /dev/null +++ b/play-java-fileupload-example/public/javascripts/hello.js @@ -0,0 +1,3 @@ +if (window.console) { + console.log("Welcome to your Play application's JavaScript!"); +} diff --git a/play-java-fileupload-example/public/stylesheets/main.css b/play-java-fileupload-example/public/stylesheets/main.css new file mode 100644 index 000000000..e69de29bb diff --git a/play-java-fileupload-example/scripts/script-helper b/play-java-fileupload-example/scripts/script-helper new file mode 100644 index 000000000..9a2faa643 --- /dev/null +++ b/play-java-fileupload-example/scripts/script-helper @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +java_version=$(java -version 2>&1 | java -version 2>&1 | awk -F '"' '/version/ {print $2}') + +if [[ $java_version = 1.8* ]] ; then + echo "The build is using Java 8 ($java_version). No addional JVM params needed." +else + echo "The build is using Java 9+ ($java_version). We need additional JVM parameters" + export _JAVA_OPTIONS="$_JAVA_OPTIONS --add-modules=java.xml.bind" +fi diff --git a/play-java-fileupload-example/scripts/test-gradle b/play-java-fileupload-example/scripts/test-gradle new file mode 100755 index 000000000..84a051a20 --- /dev/null +++ b/play-java-fileupload-example/scripts/test-gradle @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +. "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/script-helper" + +# Using cut because TRAVIS_SCALA_VERSION is the full Scala +# version (for example 2.12.4), but Gradle expects just the +# binary version (for example 2.12) +scala_binary_version=$(echo $TRAVIS_SCALA_VERSION | cut -c1-4) + +echo "+------------------------------+" +echo "| Executing tests using Gradle |" +echo "+------------------------------+" +./gradlew -Dscala.binary.version=$scala_binary_version check -i --stacktrace diff --git a/play-java-fileupload-example/scripts/test-sbt b/play-java-fileupload-example/scripts/test-sbt new file mode 100755 index 000000000..0425367b1 --- /dev/null +++ b/play-java-fileupload-example/scripts/test-sbt @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +. "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/script-helper" + +echo "+----------------------------+" +echo "| Executing tests using sbt |" +echo "+----------------------------+" +sbt ++$TRAVIS_SCALA_VERSION test diff --git a/play-java-fileupload-example/test/browsers/BrowserTest.java b/play-java-fileupload-example/test/browsers/BrowserTest.java new file mode 100644 index 000000000..f07788612 --- /dev/null +++ b/play-java-fileupload-example/test/browsers/BrowserTest.java @@ -0,0 +1,32 @@ +package browsers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.fluentlenium.core.domain.FluentWebElement; +import org.junit.Test; +import org.openqa.selenium.By; +import play.test.WithBrowser; + +import static org.junit.Assert.assertTrue; + +public class BrowserTest extends WithBrowser { + + @Test + public void uploadFile() throws IOException { + Path tmpPath = Files.createTempFile(null, null); + Files.write(tmpPath, "hello".getBytes()); + + // http://fluentlenium.org/docs/#filling-forms + // https://saucelabs.com/resources/articles/best-practices-tips-selenium-file-upload + browser.goTo("/"); + FluentWebElement nameElement = browser.find(By.name("name")).first(); + nameElement.click(); + nameElement.fill().with(tmpPath.toAbsolutePath().toString()); + nameElement.submit(); + + assertTrue(browser.pageSource().equals("file size = 5")); + } + +} diff --git a/play-java-fileupload-example/test/controllers/HomeControllerTest.java b/play-java-fileupload-example/test/controllers/HomeControllerTest.java new file mode 100644 index 000000000..199643951 --- /dev/null +++ b/play-java-fileupload-example/test/controllers/HomeControllerTest.java @@ -0,0 +1,55 @@ +package controllers; + +import akka.stream.IOResult; +import akka.stream.Materializer; +import akka.stream.javadsl.FileIO; +import akka.stream.javadsl.Source; +import akka.util.ByteString; +import org.junit.Test; +import play.Application; +import play.libs.Files; +import play.mvc.Http; +import play.mvc.Result; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.CompletionStage; + +import static java.nio.file.Files.createTempFile; +import static java.nio.file.Files.write; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static play.test.Helpers.*; + +public class HomeControllerTest { + + @Test + public void testFileUpload() { + Application app = fakeApplication(); + running(app, () -> { + try { + + Files.TemporaryFileCreator temporaryFileCreator = app.injector().instanceOf(Files.TemporaryFileCreator.class); + Materializer materializer = app.injector().instanceOf(Materializer.class); + + Path tempfilePath = createTempFile(null, "tempfile"); + write(tempfilePath, "My string to save".getBytes("utf-8")); + + Source> source = FileIO.fromPath(tempfilePath); + Http.MultipartFormData.FilePart> part = new Http.MultipartFormData.FilePart<>("name", "filename", "text/plain", source); + Http.RequestBuilder request = fakeRequest() + .method(POST) + .bodyMultipart(singletonList(part), temporaryFileCreator, materializer) + .uri("/upload"); + + Result result = route(app, request); + String actual = contentAsString(result); + assertEquals("file size = 17", actual); + } catch (IOException e) { + fail(e.getMessage()); + } + }); + } + +}