Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling multipart requests #368

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions ninja-core/src/main/java/ninja/Context.java
Expand Up @@ -41,6 +41,8 @@ public interface Context {
interface Impl extends Context {

void setRoute(Route route);

void cleanup();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can rename that to cleanupAfterRequest() + add one line of javadoc...

}

/**
Expand Down Expand Up @@ -520,8 +522,37 @@ private HTTP_STATUS(int code) {
* @return the FileItemIterator of the request or null if there was an
* error.
*/
@Deprecated
FileItemIterator getFileItemIterator();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mark getFileItemIterator deprecated?


/**
* Gets the uploaded file item for the given key from the multipart request.
* When multiple files uploaded for the same key, the first file is
* returned.
*
* @param name the key of the file
* @return file item instance corresponding to the uploaded file;
* {@code null} if no file uploaded for the given key
*/
NinjaFileItemStream getUploadedFileStream(String name);

/**
* Gets uploaded file items for the given key from the multipart request.
*
* @param name the key of files
* @return list of file item instances corresponding to uploaded files;
* empty list is returned if no files were uploaded for the given key
*/
List<NinjaFileItemStream> getUploadedFileStreams(String name);

/**
* Gets file item instances corresponding to uploaded files from multipart
* request.
*
* @return list of file items uploaded by a multipart request
*/
List<NinjaFileItemStream> getFileItems();

/**
* Get the validation context
*
Expand Down
78 changes: 78 additions & 0 deletions ninja-core/src/main/java/ninja/NinjaFileItemStream.java
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ninja;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;

import org.apache.commons.fileupload.FileItemStream;

/**
* This interface represents uploaded file of a multipart request. Uploaded
* files injected into controller methods are of this type.
*
*/
public interface NinjaFileItemStream {

/**
* Initializes this Ninja file item stream with given file item stream.
*
* @param fileItemStream file item stream to use values of
*/
void init(FileItemStream fileItemStream);

/**
* Gets an {@link InputStream} that allows to read file contents.
*
* @return input stream from which file contents can be read
* @throws IOException
*/
InputStream openStream() throws IOException;

/**
* Copies contents of this uploaded file to specified target location.
*
* @param target file to copy contents to
* @throws IOException
*/
void copyTo(Path target) throws IOException;

/**
* Gets the name of the field in the multipart form corresponding to this
* file item.
*
* @return The name of the form field.
*/
String getFieldName();

/**
* Returns the content type passed by the browser.
*
* @return The content type passed by the browser or {@code null} if not
* defined.
*/
String getContentType();

/**
* Purges underlying resources of this file item. For file system backed
* file items this method deletes temporary file where uploaded file
* contents were saved.
*/
void purge();

}
15 changes: 15 additions & 0 deletions ninja-core/src/main/java/ninja/WrappedContext.java
Expand Up @@ -222,6 +222,21 @@ public FileItemIterator getFileItemIterator() {
return wrapped.getFileItemIterator();
}

@Override
public NinjaFileItemStream getUploadedFileStream(String name) {
return wrapped.getUploadedFileStream(name);
}

@Override
public List<NinjaFileItemStream> getUploadedFileStreams(String name) {
return wrapped.getUploadedFileStreams(name);
}

@Override
public List<NinjaFileItemStream> getFileItems() {
return wrapped.getFileItems();
}

@Override
public String getRequestPath() {
return wrapped.getRequestPath();
Expand Down
54 changes: 54 additions & 0 deletions ninja-core/src/main/java/ninja/params/ArgumentExtractors.java
Expand Up @@ -20,6 +20,7 @@
import java.util.Map;

import ninja.Context;
import ninja.NinjaFileItemStream;
import ninja.session.FlashScope;
import ninja.session.Session;
import ninja.validation.Validation;
Expand Down Expand Up @@ -187,6 +188,59 @@ public String getFieldName() {
}
}

public static class FileExtractor implements ArgumentExtractor<NinjaFileItemStream> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you rename the class to NinjaFileItemStreamExtractor?


private final String name;

public FileExtractor(FileParam file) {
this.name = file.value();
}

@Override
public NinjaFileItemStream extract(Context context) {
return context.getUploadedFileStream(name);
}

@Override
public Class<NinjaFileItemStream> getExtractedType() {
return NinjaFileItemStream.class;
}

@Override
public String getFieldName() {
return name;
}
}

public static class FilesExtractor implements ArgumentExtractor<NinjaFileItemStream[]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename the class to NinjaFileItemStreamsExtractor?


private final String name;

public FilesExtractor(FileParams fileParams) {
this.name = fileParams.value();
}

@Override
public NinjaFileItemStream[] extract(Context context) {
List<NinjaFileItemStream> files = context.getUploadedFileStreams(name);
if (files == null || files.isEmpty()) {
return null;
}
return files.toArray(new NinjaFileItemStream[files.size()]);
}

@Override
public Class<NinjaFileItemStream[]> getExtractedType() {
return NinjaFileItemStream[].class;
}

@Override
public String getFieldName() {
return name;
}

}

public static class HeaderExtractor implements ArgumentExtractor<String> {
private final String key;

Expand Down
38 changes: 38 additions & 0 deletions ninja-core/src/main/java/ninja/params/FileParam.java
@@ -0,0 +1,38 @@
/**
* Copyright (C) 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ninja.params;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Inject an uploaded file to a controller method.
*
* This equals context.getUploadedFile(...)
*
* @author azilet
*/
@WithArgumentExtractor(ArgumentExtractors.FileExtractor.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface FileParam {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename that to NinjaFileItemStreamParam?


String value();

}
38 changes: 38 additions & 0 deletions ninja-core/src/main/java/ninja/params/FileParams.java
@@ -0,0 +1,38 @@
/**
* Copyright (C) 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ninja.params;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Inject multiple uploaded files with same key to a controller method.
*
* This equals context.getUploadedFiles(...)
*
* @author azilet
*/
@WithArgumentExtractor(ArgumentExtractors.FilesExtractor.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface FileParams {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to NinjaFileItemStreamsParam?


String value();

}
30 changes: 29 additions & 1 deletion ninja-core/src/main/java/ninja/utils/NinjaConstant.java
Expand Up @@ -143,7 +143,35 @@ public interface NinjaConstant {
* (XSS).
*/
final String sessionHttpOnly = "application.session.http_only";


/**
* Indicates if uploaded files should be handled fully in-memory without
* saving to file system. Can be {@code true} or {@code false}, defaults to
* {@code false}.
*/
final String FILE_UPLOADS_IN_MEMORY = "file.uploads.in_memory";

/**
* The maximum allowed size of a single uploaded file.
*
* @see org.apache.commons.fileupload.FileUploadBase#fileSizeMax
*/
final String FILE_UPLOADS_MAX_FILE_SIZE = "file.uploads.file.size.max";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could simplify that and only use "FILE_UPLOADS_MAX_REQUEST_SIZE". It somewhat also includes the max files size and we got one property less :)


/**
* The maximum allowed size of a complete request, i.e. size of all uploaded
* files.
*
* @see org.apache.commons.fileupload.FileUploadBase#sizeMax
*/
final String FILE_UPLOADS_MAX_REQUEST_SIZE = "file.uploads.total.size.max";

/**
* Directory where uploaded files are saved. Defaults to system's temporary
* directory, i.e. "java.io.tmpdir" system property is consulted
*/
final String FILE_UPLOADS_DIRECTORY = "file.uploads.directory";

// /////////////////////////////////////////////////
// Diagnostic mode - extension to dev mode where
// ninja.Ninja is forced with ninja.diagnostics.NinjaDiagnostic
Expand Down
12 changes: 12 additions & 0 deletions ninja-core/src/main/java/ninja/utils/ResultHandler.java
Expand Up @@ -16,6 +16,7 @@

package ninja.utils;


import javax.inject.Singleton;

import ninja.AsyncResult;
Expand Down Expand Up @@ -45,6 +46,17 @@ public ResultHandler(Logger logger, TemplateEngineManager templateEngineManager)
}

public void handleResult(Result result, Context context) {
try {
handleResultInternal(result, context);
} finally {
// cleanup context resources
if (context instanceof Context.Impl) {
((Context.Impl) context).cleanup();
}
}
}

private void handleResultInternal(Result result, Context context) {

if (result == null || result instanceof AsyncResult) {
// Do nothing, assuming the controller manually handled it
Expand Down
1 change: 1 addition & 0 deletions ninja-core/src/site/markdown/developer/changelog.md
Expand Up @@ -2,6 +2,7 @@ Version X.X.X
=============

* 2015-07-21 Added properties to override system views location (momiji).
* 2015-07-20 Injection of params and uploaded files from multipart requests (bazi)

Version 5.1.4
=============
Expand Down
Expand Up @@ -130,6 +130,9 @@ accepted by Joda Time library.
Therefore you don't have to worry if
input is for instance Xml or Json. You simply get a parsed object.

Additioanlly, uploaded file streams of a multipart request can be injected into your controller.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally

For more details on multipart requests please refer to
<a href="/documentation/uploading_files.html">this page</a>.

## Ninja and content negotiation

Expand Down