Skip to content

Commit 19e1986

Browse files
authored
Merge pull request #81 from prime-framework/degroff/add_default_result
Support a default result mapping on a base action
2 parents e729580 + 56f48cf commit 19e1986

File tree

8 files changed

+246
-5
lines changed

8 files changed

+246
-5
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ 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 [" +
157-
actionInvocation.uri() + "] and result code [" + getCode(forward) + "]");
156+
throw new PrimeException("Missing result for action class [" + actionInvocation.configuration.actionClass.getName() + "] URI [" +
157+
actionInvocation.uri() + "] and result code [" + getCode(forward) + "]");
158158
}
159159
return page;
160160
}

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717

1818
import java.io.IOException;
1919
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.InvocationHandler;
2021
import java.lang.reflect.Method;
22+
import java.lang.reflect.Proxy;
2123
import java.util.List;
2224
import java.util.Map;
2325

2426
import com.google.inject.Inject;
27+
import org.primeframework.mvc.PrimeException;
2528
import org.primeframework.mvc.action.ActionInvocation;
2629
import org.primeframework.mvc.action.ActionInvocationStore;
2730
import org.primeframework.mvc.action.result.ForwardResult.ForwardImpl;
@@ -93,14 +96,42 @@ public void perform(WorkflowChain chain) throws IOException {
9396
annotation = actionInvocation.configuration.resultConfigurations.get(resultCode);
9497
}
9598

99+
// If the current action has not provided a resultCode mapping, in order, resolve a default result.
100+
// Using:
101+
// 1. the bound defaultResultMappings
102+
// 2. the action defined a '*' mapping which should be used as a default.
103+
// 3. the fail-safe ForwardImpl.
104+
96105
if (annotation == null) {
97106
var defaultMapping = defaultResultMappings.get(resultCode);
98107
if (defaultMapping != null) {
99108
annotation = defaultMapping.getAnnotation(resultCode);
100109
}
101110

102111
if (annotation == null) {
103-
annotation = new ForwardImpl("", resultCode);
112+
if (actionInvocation.action != null) {
113+
// Note that if an action exists, a configuration exists, so we should not need to check for null on configuration.
114+
annotation = actionInvocation.configuration.resultConfigurations.get("*");
115+
if (annotation != null) {
116+
// Proxying the annotation will allow us to return the actual result code instead of '*' when the code() method is invoked.
117+
// - The primary reason for this is debug. See AbstractForwardResult. But any result handler could decide to use this
118+
// resultCode in theory for debug or other purposes. This proxy allows us to override the code() method.
119+
annotation = newProxyInstance(annotation, resultCode);
120+
logger.debug("Missing result annotation for action class [{}] URI [{}] for result code [{}]. A default mapping of [@{}] was configured using the [*] result code. " +
121+
"This is not an error. This message is provided in case you do wish to define an explicit mapping.",
122+
actionInvocation.configuration.actionClass.getName(), actionInvocation.uri(), resultCode, annotation.annotationType().getSimpleName());
123+
}
124+
}
125+
126+
if (annotation == null) {
127+
annotation = new ForwardImpl("", resultCode);
128+
// Only debug log this if there was an action that could have declared the result mapping.
129+
if (actionInvocation.action != null) {
130+
logger.debug("Missing result annotation for action class [{}] URI [{}] for result code [{}]. The default mapping of [@{}] was used. " +
131+
"This is not an error. This message is provided in case you do wish to define an explicit mapping.",
132+
actionInvocation.configuration.actionClass.getName(), actionInvocation.uri(), resultCode, annotation.annotationType().getSimpleName());
133+
}
134+
}
104135
}
105136
}
106137

@@ -113,6 +144,7 @@ public void perform(WorkflowChain chain) throws IOException {
113144
}
114145

115146
long start = System.currentTimeMillis();
147+
@SuppressWarnings("rawtypes")
116148
Result result = factory.build(annotation.annotationType());
117149
boolean handled = result.execute(annotation);
118150

@@ -131,6 +163,7 @@ private void handleContinueOrRedirect(WorkflowChain chain) throws IOException {
131163
String uri = resourceLocator.locateIndex(configuration.templateDirectory());
132164
if (uri != null) {
133165
Annotation annotation = new RedirectImpl(uri, "success", true, false);
166+
@SuppressWarnings("rawtypes")
134167
Result redirectResult = factory.build(annotation.annotationType());
135168
redirectResult.execute(annotation);
136169
return;
@@ -140,4 +173,39 @@ private void handleContinueOrRedirect(WorkflowChain chain) throws IOException {
140173
// In either case, this should be a 404 so we will let the Prime MVC workflow at the top level handle that
141174
chain.continueWorkflow();
142175
}
176+
177+
private Annotation newProxyInstance(Annotation annotation, String resultCode) {
178+
try {
179+
// Note that the annotation object here is already a proxy. Asking this object for getClass() will not return the correct class.
180+
Class<?> annotationType = annotation.annotationType();
181+
return (Annotation) Proxy.newProxyInstance(
182+
annotationType.getClassLoader(),
183+
new Class[]{annotationType},
184+
new AnnotationInvocationHandler(annotation, resultCode));
185+
} catch (Exception e) {
186+
throw new PrimeException("Unable to proxy the default result code [*]. This is unexpected.", e);
187+
}
188+
}
189+
190+
public static class AnnotationInvocationHandler implements InvocationHandler {
191+
private final Annotation annotation;
192+
193+
private final String resultCode;
194+
195+
public AnnotationInvocationHandler(Annotation annotation, String resultCode) {
196+
this.annotation = annotation;
197+
this.resultCode = resultCode;
198+
}
199+
200+
@Override
201+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
202+
// The purpose of this proxy handler is to be able to modify the return value for "code" which is the result code.
203+
if (method.getName().equals("code")) {
204+
return resultCode;
205+
}
206+
207+
// For all other methods, defer to the actual annotation
208+
return method.invoke(annotation, args);
209+
}
210+
}
143211
}

src/test/java/org/example/action/DefaultForwardResultAction.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, Inversoft Inc., All Rights Reserved
2+
* Copyright (c) 2015-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.
@@ -24,8 +24,9 @@
2424
@Action
2525
@Forward
2626
public class DefaultForwardResultAction {
27+
public String resultCode = "success";
2728

2829
public String get() {
29-
return "success";
30+
return resultCode;
3031
}
3132
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.example.action;
17+
18+
import org.primeframework.mvc.action.annotation.Action;
19+
import org.primeframework.mvc.action.result.annotation.Forward;
20+
21+
/**
22+
* @author Daniel DeGroff
23+
*/
24+
@Action
25+
@Forward.List({
26+
@Forward,
27+
@Forward(code = "*", status = 201),
28+
})
29+
public class RequestedDefaultForwardResultAction {
30+
public String resultCode = "success";
31+
32+
public String get() {
33+
return resultCode;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.example.action;
17+
18+
import org.primeframework.mvc.action.annotation.Action;
19+
import org.primeframework.mvc.action.result.annotation.Forward;
20+
21+
/**
22+
* @author Daniel DeGroff
23+
*/
24+
@Action
25+
@Forward.List({
26+
@Forward,
27+
@Forward(code = "*", status = 201),
28+
})
29+
public class RequestedDefaultForwardResultNoTemplateAction {
30+
public String resultCode = "success";
31+
32+
public String get() {
33+
return resultCode;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.example.action;
17+
18+
import org.primeframework.mvc.action.annotation.Action;
19+
import org.primeframework.mvc.action.result.annotation.Status;
20+
21+
/**
22+
* @author Daniel DeGroff
23+
*/
24+
@Action
25+
@Status.List({
26+
@Status,
27+
@Status(code = "*", status = 201),
28+
})
29+
public class RequestedDefaultStatusResultAction {
30+
public String resultCode = "success";
31+
32+
public String get() {
33+
return resultCode;
34+
}
35+
}

src/test/java/org/primeframework/mvc/GlobalTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,71 @@ public void get_collectionConverter() throws Exception {
436436
.assertBodyContains("<input type=\"text\" id=\"strings\" name=\"strings\" value=\"foo,bar,baz\"/"));
437437
}
438438

439+
@Test
440+
public void get_default_results() {
441+
// Take default of 'success' See DefaultForwardResultAction
442+
simulator.test("/default-forward-result")
443+
.get()
444+
.assertStatusCode(200)
445+
.assertBody("""
446+
Default Forward""");
447+
TestUnhandledExceptionHandler.assertNoUnhandledException();
448+
449+
// Unknown result code, still a 200 using ForwardImpl because this is the default in DefaultResultInvocationWorkflow
450+
simulator.test("/default-forward-result")
451+
.withURLParameter("resultCode", "foo")
452+
.get()
453+
.assertStatusCode(200)
454+
.assertBody("""
455+
Default Forward""");
456+
TestUnhandledExceptionHandler.assertNoUnhandledException();
457+
458+
// Take default of 'success' See RequestedDefaultForwardResultAction
459+
simulator.test("/requested-default-status-result")
460+
.get()
461+
.assertStatusCode(200)
462+
.assertBodyIsEmpty();
463+
TestUnhandledExceptionHandler.assertNoUnhandledException();
464+
465+
// Unknown result code, this should use the default mapping of '*' which results in a 201
466+
simulator.test("/requested-default-status-result")
467+
.withURLParameter("resultCode", "foo")
468+
.get()
469+
.assertStatusCode(201)
470+
.assertBodyIsEmpty();
471+
TestUnhandledExceptionHandler.assertNoUnhandledException();
472+
473+
// Ensure the normal mapping works, expecting an .ftl per the forward result
474+
simulator.test("/requested-default-forward-result")
475+
.get()
476+
.assertStatusCode(200)
477+
.assertBody("""
478+
Hi!
479+
""");
480+
TestUnhandledExceptionHandler.assertNoUnhandledException();
481+
482+
// Unknown result code of 'foo', this should use the default mapping of '*' which results in a 201
483+
simulator.test("/requested-default-forward-result")
484+
.withURLParameter("resultCode", "foo")
485+
.get()
486+
.assertStatusCode(201)
487+
.assertBody("""
488+
Hi!
489+
""");
490+
TestUnhandledExceptionHandler.assertNoUnhandledException();
491+
492+
// No template, so expect an exception. Ensure the exception has the correct result code.
493+
simulator.test("/requested-default-forward-result-no-template")
494+
.withURLParameter("resultCode", "foo")
495+
.get()
496+
// 500 because the template is missing
497+
.assertStatusCode(500);
498+
499+
// Ensure the exception contains the invalid result code of 'foo' and not '*' which is found in the default mapping.
500+
TestUnhandledExceptionHandler.assertLastUnhandledException(new PrimeException(
501+
"Missing result for action class [org.example.action.RequestedDefaultForwardResultNoTemplateAction] URI [/requested-default-forward-result-no-template] and result code [foo]"));
502+
}
503+
439504
@Test
440505
public void get_developmentExceptions() {
441506
// Bad annotation @Action("{id}") it should be @Action("{uuid}")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[#ftl/]
2+
Hi!

0 commit comments

Comments
 (0)