Skip to content

Commit 340b7b0

Browse files
authored
Merge pull request #80 from prime-framework/degroff/file_manager_mainline
merge / copy over changes from 4.x maintenance
2 parents 446eee6 + 1597197 commit 340b7b0

40 files changed

+734
-212
lines changed

build.savant

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dropWizardVersion = "3.2.6"
1919
easyMockVersion = "5.2.0"
2020
freemarkerVersion = "2.3.32"
2121
fusionAuthJWTVersion = "5.3.2"
22-
javaHTTPVersion = "1.0.0"
22+
javaHTTPVersion = "1.1.0"
2323
jsonPatchVersion = "1.13.0"
2424
guavaVersion = "32.1.2-jre"
2525
guiceVersion = "6.0.0"
@@ -29,7 +29,7 @@ logbackVersion = "1.4.14"
2929
slf4jVersion = "2.0.13"
3030
testngVersion = "7.8.0"
3131

32-
project(group: "org.primeframework", name: "prime-mvc", version: "5.0.0", licenses: ["ApacheV2_0"]) {
32+
project(group: "org.primeframework", name: "prime-mvc", version: "5.1.0", licenses: ["ApacheV2_0"]) {
3333
workflow {
3434
fetch {
3535
// Dependency resolution order:

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>org.primeframework</groupId>
77
<artifactId>prime-mvc</artifactId>
8-
<version>5.0.0</version>
8+
<version>5.1.0</version>
99
<packaging>jar</packaging>
1010

1111
<name>FusionAuth App</name>
@@ -109,7 +109,7 @@
109109
<dependency>
110110
<groupId>io.fusionauth</groupId>
111111
<artifactId>java-http</artifactId>
112-
<version>1.0.0</version>
112+
<version>1.1.0</version>
113113
<type>jar</type>
114114
<scope>compile</scope>
115115
<optional>false</optional>

prime-mvc.iml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@
126126
<orderEntry type="module-library">
127127
<library>
128128
<CLASSES>
129-
<root url="jar://$MODULE_DIR$/.savant/cache/io/fusionauth/java-http/1.0.0/java-http-1.0.0.jar!/" />
129+
<root url="jar://$MODULE_DIR$/.savant/cache/io/fusionauth/java-http/1.1.0/java-http-1.1.0.jar!/" />
130130
</CLASSES>
131131
<JAVADOC />
132132
<SOURCES>
133-
<root url="jar://$MODULE_DIR$/.savant/cache/io/fusionauth/java-http/1.0.0/java-http-1.0.0-src.jar!/" />
133+
<root url="jar://$MODULE_DIR$/.savant/cache/io/fusionauth/java-http/1.1.0/java-http-1.1.0-src.jar!/" />
134134
</SOURCES>
135135
</library>
136136
</orderEntry>

src/main/java/org/primeframework/mvc/BasePrimeMain.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021-2023, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2021-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import com.google.inject.Module;
2323
import io.fusionauth.http.server.HTTPServer;
2424
import io.fusionauth.http.server.HTTPServerConfiguration;
25+
import org.primeframework.mvc.config.MVCConfiguration;
2526
import org.primeframework.mvc.guice.GuiceBootstrap;
2627
import org.primeframework.mvc.log.SLF4JLoggerFactoryAdapter;
2728

@@ -107,6 +108,19 @@ public void start() {
107108
// Set the logger factory for the server.
108109
configureLoggerFactory(config);
109110

111+
// prime and the HTTP server both have a configuration for max file size on an upload request.
112+
// - Ensure they are compatible.
113+
// Note that the prime-mvc check must wait for the file to be written to disk, so it does not keep the file from being written.
114+
// It waits until the file is written by the HTTP server and then fails nicely to let the end user know it is too big.
115+
// Note that the java-http check will be performed during upload, so if it fails, the prime-mvc request handler may or may not complete. This
116+
// will depend upon when the request body is read which is up to the request handler.
117+
MVCConfiguration mvcConfiguration = injector.getInstance(MVCConfiguration.class);
118+
long mvcMaxFileSize = mvcConfiguration.fileUploadMaxSize();
119+
long httpMaxFileSize = config.getMultipartConfiguration().getMaxFileSize();
120+
if (mvcMaxFileSize < httpMaxFileSize) {
121+
throw new IllegalStateException("MVCConfiguration.fileUploadMaxSize must be greater than or equal to the HTTP server configuration. Prime MVC configuration [" + mvcMaxFileSize + "] HTTP server configuration [" + httpMaxFileSize + "]");
122+
}
123+
110124
// Create the server
111125
var server = new HTTPServer().withConfiguration(config)
112126
.withHandler(requestHandler)

src/main/java/org/primeframework/mvc/action/ActionMapper.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2001-2007, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2001-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,11 +26,9 @@ public interface ActionMapper {
2626
/**
2727
* Maps the given URI to an action invocation.
2828
*
29-
* @param httpMethod The HTTP method being invoked.
30-
* @param uri The URI.
31-
* @param executeResult This flag is set into the ActionInvocation to control whether or not the result is executed or
32-
* not.
29+
* @param httpMethod The HTTP method being invoked.
30+
* @param uri The URI.
3331
* @return The action invocation and never null. This invocation might be a redirect for index handling.
3432
*/
35-
ActionInvocation map(HTTPMethod httpMethod, String uri, boolean executeResult);
36-
}
33+
ActionInvocation map(HTTPMethod httpMethod, String uri);
34+
}

src/main/java/org/primeframework/mvc/action/DefaultActionMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2001-2020, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2001-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,7 +43,7 @@ public DefaultActionMapper(ActionConfigurationProvider actionConfigurationProvid
4343
/**
4444
* {@inheritDoc}
4545
*/
46-
public ActionInvocation map(HTTPMethod httpMethod, String uri, boolean executeResult) {
46+
public ActionInvocation map(HTTPMethod httpMethod, String uri) {
4747
ActionInvocation invocation = actionConfigurationProvider.lookup(uri);
4848
if (invocation.configuration == null && !uri.endsWith("/")) {
4949
// Do an index check but if it doesn't return a valid invocation, then return the original one from above.
@@ -70,4 +70,4 @@ public ActionInvocation map(HTTPMethod httpMethod, String uri, boolean executeRe
7070

7171
return invocation;
7272
}
73-
}
73+
}

src/main/java/org/primeframework/mvc/action/DefaultActionMappingWorkflow.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import com.codahale.metrics.Timer;
2323
import com.google.inject.Inject;
2424
import io.fusionauth.http.HTTPMethod;
25+
import io.fusionauth.http.io.MultipartConfiguration;
26+
import io.fusionauth.http.io.MultipartFileUploadPolicy;
2527
import io.fusionauth.http.server.HTTPRequest;
2628
import io.fusionauth.http.server.HTTPResponse;
2729
import org.primeframework.mvc.NotAllowedException;
2830
import org.primeframework.mvc.http.HTTPTools;
2931
import org.primeframework.mvc.http.Status;
30-
import org.primeframework.mvc.parameter.InternalParameters;
32+
import org.primeframework.mvc.parameter.fileupload.annotation.FileUpload;
3133
import org.primeframework.mvc.workflow.WorkflowChain;
3234
import org.slf4j.Logger;
3335
import org.slf4j.LoggerFactory;
@@ -74,8 +76,7 @@ public void perform(WorkflowChain chain) throws IOException {
7476
}
7577

7678
HTTPMethod method = request.getMethod();
77-
boolean executeResult = InternalParameters.is(request, InternalParameters.EXECUTE_RESULT);
78-
ActionInvocation actionInvocation = actionMapper.map(method, uri, executeResult);
79+
ActionInvocation actionInvocation = actionMapper.map(method, uri);
7980

8081
// This case is a redirect because the URI maps to something new and there isn't an action associated with it. For
8182
// example, this is how the index handling works.
@@ -95,6 +96,9 @@ public void perform(WorkflowChain chain) throws IOException {
9596
throw new NotAllowedException();
9697
}
9798

99+
// Handle multipart file configuration
100+
handleMultiPartConfiguration(actionInvocation);
101+
98102
// Start the timers and grab some meters for errors
99103
Timer.Context perPathTimer = null;
100104
Timer.Context aggregateTimer = null;
@@ -147,4 +151,45 @@ private String determineURI() {
147151

148152
return uri;
149153
}
154+
155+
private void handleMultiPartConfiguration(ActionInvocation actionInvocation) {
156+
if (actionInvocation.configuration == null) {
157+
return;
158+
}
159+
160+
// Note that multipart file handling is disabled by default. Enable it if the action has indicated it is expecting a file upload.
161+
// - It is possible the default has been changed. This is ok, if the action invocation is not expecting any files, keep the default as configured
162+
// by the implementation.
163+
boolean expectingFileUploads = !actionInvocation.configuration.fileUploadMembers.isEmpty();
164+
if (!expectingFileUploads) {
165+
return;
166+
}
167+
168+
MultipartConfiguration multipartConfiguration = request.getMultiPartStreamProcessor().getMultiPartConfiguration();
169+
multipartConfiguration.withFileUploadPolicy(MultipartFileUploadPolicy.Allow);
170+
171+
// Take the largest configured file size, or if none have specified a max file size, use the configured default.
172+
long configuredMaxFileSize = multipartConfiguration.getMaxFileSize();
173+
var fileUploadMembers = actionInvocation.configuration.fileUploadMembers;
174+
long maxFileSize = fileUploadMembers.values().stream()
175+
.map(FileUpload::maxSize)
176+
.filter(m -> m != -1)
177+
.max(Long::compareTo)
178+
.orElse(configuredMaxFileSize);
179+
multipartConfiguration.withMaxFileSize(maxFileSize);
180+
181+
// If each file specifies a max size, then this will be the sum. It may also be 0 if no FileUpload members specified a max size.
182+
long expectedFileSizes = fileUploadMembers.values().stream()
183+
.map(FileUpload::maxSize)
184+
.filter(m -> m != -1)
185+
.count();
186+
long calculatedExpectedFileSize = maxFileSize * fileUploadMembers.size();
187+
188+
// Take the larger of the two and add 1MB
189+
// - The 1MB is just a guess, and provides some overhead for anything else in the request body such as form-data.
190+
long adjustedMaxRequestSize = Math.max(expectedFileSizes, calculatedExpectedFileSize) + (1024 * 1024);
191+
if (Math.max(expectedFileSizes, calculatedExpectedFileSize) > multipartConfiguration.getMaxRequestSize()) {
192+
multipartConfiguration.withMaxRequestSize(adjustedMaxRequestSize);
193+
}
194+
}
150195
}

src/main/java/org/primeframework/mvc/action/guice/ActionModule.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2012-2025, Inversoft Inc., All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717

1818
import com.google.inject.AbstractModule;
1919
import com.google.inject.Singleton;
20+
import com.google.inject.multibindings.MapBinder;
2021
import org.primeframework.mvc.action.ActionInvocationStore;
2122
import org.primeframework.mvc.action.ActionInvocationWorkflow;
2223
import org.primeframework.mvc.action.ActionMapper;
@@ -30,11 +31,13 @@
3031
import org.primeframework.mvc.action.config.ActionConfigurationProvider;
3132
import org.primeframework.mvc.action.config.DefaultActionConfigurationBuilder;
3233
import org.primeframework.mvc.action.config.DefaultActionConfigurationProvider;
34+
import org.primeframework.mvc.action.result.ActionResultDefinition;
3335
import org.primeframework.mvc.action.result.DefaultResourceLocator;
3436
import org.primeframework.mvc.action.result.DefaultResultInvocationWorkflow;
3537
import org.primeframework.mvc.action.result.ResourceLocator;
3638
import org.primeframework.mvc.action.result.ResultInvocationWorkflow;
3739
import org.primeframework.mvc.action.result.ResultStore;
40+
import org.primeframework.mvc.action.result.StatusActionResultDefinition;
3841
import org.primeframework.mvc.action.result.ThreadLocalResultStore;
3942

4043
/**
@@ -51,6 +54,14 @@ protected void bindConfigurationProvider() {
5154
bind(ActionConfigurationProvider.class).to(DefaultActionConfigurationProvider.class).in(Singleton.class);
5255
}
5356

57+
protected void bindDefaultResultMappings() {
58+
MapBinder<String, ActionResultDefinition> defaultResultMappings = MapBinder.newMapBinder(binder(), String.class, ActionResultDefinition.class);
59+
defaultResultMappings.addBinding("not-allowed").toInstance(new StatusActionResultDefinition(405));
60+
defaultResultMappings.addBinding("content-too-large").toInstance(new StatusActionResultDefinition(413));
61+
defaultResultMappings.addBinding("unprocessable-content").toInstance(new StatusActionResultDefinition(422));
62+
defaultResultMappings.addBinding("not-implemented").toInstance(new StatusActionResultDefinition(501));
63+
}
64+
5465
protected void bindMapper() {
5566
bind(ActionMapper.class).to(DefaultActionMapper.class);
5667
}
@@ -87,6 +98,9 @@ protected void configure() {
8798
bindConfigurationBuilder();
8899
bindConfigurationProvider();
89100

101+
// Default result mappings
102+
bindDefaultResultMappings();
103+
90104
// Invocation
91105
bindStore();
92106
bindWorkflow();

src/main/java/org/primeframework/mvc/action/result/AbstractForwardResult.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ private String buildFullyQualifiedPath(ActionInvocation actionInvocation, U forw
153153
private String locateDefault(ActionInvocation actionInvocation, U forward) {
154154
String page = resourceLocator.locate(configuration.templateDirectory());
155155
if (page == null) {
156-
throw new PrimeException("Missing result for action class [" + actionInvocation.configuration.actionClass + "] URI [" +
156+
throw new PrimeException("Missing result for action class [" + actionInvocation.configuration.actionClass.getName() + "] URI [" +
157157
actionInvocation.uri() + "] and result code [" + getCode(forward) + "]");
158158
}
159159
return page;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2025, Inversoft Inc., All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package org.primeframework.mvc.action.result;
17+
18+
import java.lang.annotation.Annotation;
19+
20+
/**
21+
* An interface to define an action result definition. This can be bound and replace having to add a result annotation to each action.
22+
*
23+
* @author Daniel DeGroff
24+
*/
25+
public interface ActionResultDefinition {
26+
/**
27+
* @param resultCode the result code to use for the mapping
28+
* @return the constructed annotation.
29+
*/
30+
Annotation getAnnotation(String resultCode);
31+
32+
/**
33+
* @return the status code for the HTTP response
34+
*/
35+
int getStatus();
36+
}

0 commit comments

Comments
 (0)