Skip to content

Commit 5023bff

Browse files
authored
Merge pull request jooby-project#1969 from imeszaros/param-lookup
Parameter lookup feature for script and MVC routes
2 parents a2691b0 + 2c9f665 commit 5023bff

15 files changed

Lines changed: 834 additions & 18 deletions

File tree

‎docs/asciidoc/context.adoc‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ registry while the request it belongs to is being processed.
4444

4545
=== Parameters
4646

47-
There are several parameter types: `header`, `cookie`, `path`, `query`, `form` and `multipart`.
48-
All them share an unified/type-safe API for accessing and manipulating their values.
47+
There are several parameter types: `header`, `cookie`, `path`, `query`, `form`, `multipart`,
48+
`session` and `flash`. All them share a unified/type-safe API for accessing and manipulating their values.
4949

5050
We are going to describe them briefly in the next sections, then go into specific features of the
5151
<<Value API>>.
5252

53+
There is also a <<context-parameters-parameter-lookup,parameter lookup>> feature by which you can access a parameter from any combination
54+
of the above types with well-defined priority.
55+
5356
==== Header
5457

5558
HTTP headers allow the client and the server to pass additional information with the request or the
@@ -568,6 +571,41 @@ Flash attributes are implemented using an `HTTP Cookie`. To customize the cookie
568571
install(FlashScope(Cookie("myflash").setHttpOnly(true)))
569572
----
570573

574+
==== Parameter Lookup
575+
576+
You can search for parameters in multiple sources with an explicitly defined priority using the
577+
javadoc:Context[lookup] or javadoc:Context[lookup, java.lang.String, ParamSource...] method:
578+
579+
.Java
580+
[source,java,role="primary"]
581+
----
582+
get("/{foo}", ctx -> {
583+
String foo = ctx.lookup("foo", ParamSource.QUERY, ParamSource.PATH).value();
584+
return "foo is: " + foo;
585+
});
586+
587+
get("/{foo}", ctx -> {
588+
String foo = ctx.lookup().inQuery().inPath().get("foo").value();
589+
return "foo is: " + foo;
590+
});
591+
----
592+
593+
.Kotlin
594+
[source,kotlin,role="secondary"]
595+
----
596+
get("/{foo}") { ctx ->
597+
val foo = ctx.lookup("foo", ParamSource.QUERY, ParamSource.PATH).value()
598+
"foo is: $foo"
599+
}
600+
601+
get("/{foo}") { ctx ->
602+
val foo = ctx.lookup().inQuery().inPath()["foo"].value()
603+
"foo is: $foo"
604+
}
605+
----
606+
607+
In case of a request like `/bar?foo=baz`, `foo is: baz` will be returned since the query parameter
608+
takes precedence over the path parameter.
571609

572610
include::value-api.adoc[]
573611

‎docs/asciidoc/mvc-api.adoc‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ MVC routes (Singleton vs Non-Singleton routes).
220220

221221
HTTP parameter provision is available via `*Param` annotations.
222222

223+
There is also a javadoc:annotations.Param[] annotation which allows to retrieve parameters from
224+
<<mvc-api-parameters-multiple-sources,multiple sources>>.
225+
223226
==== Header
224227

225228
Provisioning of headers is available via javadoc:annotations.HeaderParam[] annotation:
@@ -600,6 +603,36 @@ class MyController {
600603

601604
<1> All context attributes must be set as arguments. They must be declared as `Map<String, Object>`
602605

606+
==== Multiple Sources
607+
608+
You can use the javadoc:annotations.Param[] annotation to search for a parameter in multiple sources.
609+
The sources and their precedence can be specified as follows:
610+
611+
.Multiple Sources
612+
[source, java, role = "primary"]
613+
----
614+
public class FooController {
615+
616+
@GET("/{foo}")
617+
public String multipleSources(@Param({ QUERY, PATH }) String foo) {
618+
return "foo is: " + foo;
619+
}
620+
}
621+
----
622+
623+
.Kotlin
624+
[source, kotlin, role = "secondary"]
625+
----
626+
class FooController {
627+
628+
@GET("/{foo}")
629+
fun multipleSources(@Param(QUERY, PATH) foo: String) = "foo is: $foo"
630+
}
631+
----
632+
633+
In case of a request like `/bar?foo=baz`, `foo is: baz` will be returned since the query parameter
634+
takes precedence over the path parameter.
635+
603636
=== Responses
604637

605638
==== Status Code

‎jooby/src/main/java/io/jooby/Context.java‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby;
77

8+
import io.jooby.internal.ParamLookupImpl;
89
import io.jooby.internal.ReadOnlyContext;
910
import io.jooby.internal.WebSocketSender;
1011

@@ -22,6 +23,7 @@
2223
import java.time.Instant;
2324
import java.time.ZoneId;
2425
import java.time.format.DateTimeFormatter;
26+
import java.util.Arrays;
2527
import java.util.Date;
2628
import java.util.List;
2729
import java.util.Locale;
@@ -715,6 +717,55 @@ public interface Context extends Registry {
715717
*/
716718
@Nonnull FileUpload file(@Nonnull String name);
717719

720+
/* **********************************************************************************************
721+
* Parameter Lookup
722+
* **********************************************************************************************
723+
*/
724+
725+
/**
726+
* Searches for a parameter in the specified sources, in the specified
727+
* order, returning the first non-missing {@link Value}, or a 'missing'
728+
* {@link Value} if none found.
729+
* <p>
730+
* At least one {@link ParamSource} must be specified.
731+
*
732+
* @param name The name of the parameter.
733+
* @param sources Sources to search in.
734+
* @return The first non-missing {@link Value} or a {@link Value} representing
735+
* a missing value if none found.
736+
* @throws IllegalArgumentException If no {@link ParamSource}s are specified.
737+
*/
738+
default Value lookup(String name, ParamSource... sources) {
739+
if (sources.length == 0) {
740+
throw new IllegalArgumentException("No parameter sources were specified.");
741+
}
742+
743+
return Arrays.stream(sources)
744+
.map(source -> source.provider.apply(this, name))
745+
.filter(value -> !value.isMissing())
746+
.findFirst()
747+
.orElseGet(() -> Value.missing(name));
748+
}
749+
750+
/**
751+
* Returns a {@link ParamLookup} instance which is a fluent interface covering
752+
* the functionality of the {@link #lookup(String, ParamSource...)} method.
753+
*
754+
* <pre>{@code
755+
* Value foo = ctx.lookup()
756+
* .inQuery()
757+
* .inPath()
758+
* .get("foo");
759+
* }</pre>
760+
*
761+
* @return A {@link ParamLookup} instance.
762+
* @see ParamLookup
763+
* @see #lookup(String, ParamSource...)
764+
*/
765+
default ParamLookup lookup() {
766+
return new ParamLookupImpl(this);
767+
}
768+
718769
/* **********************************************************************************************
719770
* Request Body
720771
* **********************************************************************************************
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby;
7+
8+
import java.util.Optional;
9+
10+
/**
11+
* Fluent interface allowing to conveniently search context parameters
12+
* in multiple sources.
13+
*
14+
* <pre>{@code
15+
* Value foo = ctx.lookup()
16+
* .inQuery()
17+
* .inPath()
18+
* .get("foo");
19+
* }</pre>
20+
*
21+
* @see Context#lookup()
22+
* @see Context#lookup(String, ParamSource...)
23+
*/
24+
public interface ParamLookup {
25+
26+
/**
27+
* Adds the specified source to the search locations.
28+
*
29+
* @param source The source to add.
30+
* @return This instance.
31+
*/
32+
Stage in(ParamSource source);
33+
34+
/**
35+
* Adds the path parameters to the search locations.
36+
*
37+
* @return This instance.
38+
*/
39+
default Stage inPath() {
40+
return in(ParamSource.PATH);
41+
}
42+
43+
/**
44+
* Adds the header parameters to the search locations.
45+
*
46+
* @return This instance.
47+
*/
48+
default Stage inHeader() {
49+
return in(ParamSource.HEADER);
50+
}
51+
52+
/**
53+
* Adds the cookie parameters to the search locations.
54+
*
55+
* @return This instance.
56+
*/
57+
default Stage inCookie() {
58+
return in(ParamSource.COOKIE);
59+
}
60+
61+
/**
62+
* Adds the flash parameters to the search locations.
63+
*
64+
* @return This instance.
65+
*/
66+
default Stage inFlash() {
67+
return in(ParamSource.FLASH);
68+
}
69+
70+
/**
71+
* Adds the session parameters to the search locations.
72+
*
73+
* @return This instance.
74+
*/
75+
default Stage inSession() {
76+
return in(ParamSource.SESSION);
77+
}
78+
79+
/**
80+
* Adds the query parameters to the search locations.
81+
*
82+
* @return This instance.
83+
*/
84+
default Stage inQuery() {
85+
return in(ParamSource.QUERY);
86+
}
87+
88+
/**
89+
* Adds the form parameters to the search locations.
90+
*
91+
* @return This instance.
92+
*/
93+
default Stage inForm() {
94+
return in(ParamSource.FORM);
95+
}
96+
97+
/**
98+
* Adds the multipart parameters to the search locations.
99+
*
100+
* @return This instance.
101+
*/
102+
default Stage inMultipart() {
103+
return in(ParamSource.MULTIPART);
104+
}
105+
106+
/**
107+
* Fluent interface allowing to conveniently search context parameters
108+
* in multiple sources.
109+
*
110+
* <pre>{@code
111+
* Value foo = ctx.lookup()
112+
* .inQuery()
113+
* .inPath()
114+
* .get("foo");
115+
* }</pre>
116+
*
117+
* @see Context#lookup()
118+
* @see Context#lookup(String, ParamSource...)
119+
*/
120+
interface Stage extends ParamLookup {
121+
122+
/**
123+
* Searches for a parameter in the specified sources, in the specified
124+
* order, returning the first non-missing {@link Value}, or a 'missing'
125+
* {@link Value} if none found.
126+
*
127+
* @param name The name of the parameter.
128+
* @return The first non-missing {@link Value} or a {@link Value} representing
129+
* a missing value if none found.
130+
*/
131+
Value get(String name);
132+
133+
/**
134+
* Wraps the result of {@link #get(String)} in an {@link Optional} if the
135+
* value is a {@link ValueNode} or returns an empty {@link Optional}
136+
* otherwise.
137+
*
138+
* @param name The name of the parameter.
139+
* @return An {@link Optional} wrapping the result of {@link #get(String)}
140+
*/
141+
default Optional<ValueNode> getNode(String name) {
142+
return Optional.of(get(name))
143+
.map(v -> v instanceof ValueNode ? (ValueNode) v : null);
144+
}
145+
}
146+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby;
7+
8+
import java.util.function.BiFunction;
9+
10+
/**
11+
* List of possible parameter sources supported by {@link Context#lookup(String, ParamSource...)}.
12+
*
13+
* @see io.jooby.annotations.Param
14+
* @see Context#lookup(String, ParamSource...)
15+
*/
16+
public enum ParamSource {
17+
18+
/**
19+
* Source equivalent to {@link Context#path(String)}.
20+
*/
21+
PATH(Context::path),
22+
23+
/**
24+
* Source equivalent to {@link Context#header(String)}.
25+
*/
26+
HEADER(Context::header),
27+
28+
/**
29+
* Source equivalent to {@link Context#cookie(String)}.
30+
*/
31+
COOKIE(Context::cookie),
32+
33+
/**
34+
* Source equivalent to {@link Context#flash(String)}.
35+
*/
36+
FLASH(Context::flash),
37+
38+
/**
39+
* Source equivalent to {@link Context#session(String)}.
40+
*/
41+
SESSION(Context::session),
42+
43+
/**
44+
* Source equivalent to {@link Context#query(String)}.
45+
*/
46+
QUERY(Context::query),
47+
48+
/**
49+
* Source equivalent to {@link Context#form(String)}.
50+
*/
51+
FORM(Context::form),
52+
53+
/**
54+
* Source equivalent to {@link Context#multipart(String)}.
55+
*/
56+
MULTIPART(Context::multipart);
57+
58+
final BiFunction<Context, String, Value> provider;
59+
60+
ParamSource(BiFunction<Context, String, Value> provider) {
61+
this.provider = provider;
62+
}
63+
}

0 commit comments

Comments
 (0)