Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.jooby.Route Maven / Gradle / Ivy
/**
* Apache License
* Version 2.0, January 2004
* http://www.apache.org/licenses/
*
* TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
*
* 1. Definitions.
*
* "License" shall mean the terms and conditions for use, reproduction,
* and distribution as defined by Sections 1 through 9 of this document.
*
* "Licensor" shall mean the copyright owner or entity authorized by
* the copyright owner that is granting the License.
*
* "Legal Entity" shall mean the union of the acting entity and all
* other entities that control, are controlled by, or are under common
* control with that entity. For the purposes of this definition,
* "control" means (i) the power, direct or indirect, to cause the
* direction or management of such entity, whether by contract or
* otherwise, or (ii) ownership of fifty percent (50%) or more of the
* outstanding shares, or (iii) beneficial ownership of such entity.
*
* "You" (or "Your") shall mean an individual or Legal Entity
* exercising permissions granted by this License.
*
* "Source" form shall mean the preferred form for making modifications,
* including but not limited to software source code, documentation
* source, and configuration files.
*
* "Object" form shall mean any form resulting from mechanical
* transformation or translation of a Source form, including but
* not limited to compiled object code, generated documentation,
* and conversions to other media types.
*
* "Work" shall mean the work of authorship, whether in Source or
* Object form, made available under the License, as indicated by a
* copyright notice that is included in or attached to the work
* (an example is provided in the Appendix below).
*
* "Derivative Works" shall mean any work, whether in Source or Object
* form, that is based on (or derived from) the Work and for which the
* editorial revisions, annotations, elaborations, or other modifications
* represent, as a whole, an original work of authorship. For the purposes
* of this License, Derivative Works shall not include works that remain
* separable from, or merely link (or bind by name) to the interfaces of,
* the Work and Derivative Works thereof.
*
* "Contribution" shall mean any work of authorship, including
* the original version of the Work and any modifications or additions
* to that Work or Derivative Works thereof, that is intentionally
* submitted to Licensor for inclusion in the Work by the copyright owner
* or by an individual or Legal Entity authorized to submit on behalf of
* the copyright owner. For the purposes of this definition, "submitted"
* means any form of electronic, verbal, or written communication sent
* to the Licensor or its representatives, including but not limited to
* communication on electronic mailing lists, source code control systems,
* and issue tracking systems that are managed by, or on behalf of, the
* Licensor for the purpose of discussing and improving the Work, but
* excluding communication that is conspicuously marked or otherwise
* designated in writing by the copyright owner as "Not a Contribution."
*
* "Contributor" shall mean Licensor and any individual or Legal Entity
* on behalf of whom a Contribution has been received by Licensor and
* subsequently incorporated within the Work.
*
* 2. Grant of Copyright License. Subject to the terms and conditions of
* this License, each Contributor hereby grants to You a perpetual,
* worldwide, non-exclusive, no-charge, royalty-free, irrevocable
* copyright license to reproduce, prepare Derivative Works of,
* publicly display, publicly perform, sublicense, and distribute the
* Work and such Derivative Works in Source or Object form.
*
* 3. Grant of Patent License. Subject to the terms and conditions of
* this License, each Contributor hereby grants to You a perpetual,
* worldwide, non-exclusive, no-charge, royalty-free, irrevocable
* (except as stated in this section) patent license to make, have made,
* use, offer to sell, sell, import, and otherwise transfer the Work,
* where such license applies only to those patent claims licensable
* by such Contributor that are necessarily infringed by their
* Contribution(s) alone or by combination of their Contribution(s)
* with the Work to which such Contribution(s) was submitted. If You
* institute patent litigation against any entity (including a
* cross-claim or counterclaim in a lawsuit) alleging that the Work
* or a Contribution incorporated within the Work constitutes direct
* or contributory patent infringement, then any patent licenses
* granted to You under this License for that Work shall terminate
* as of the date such litigation is filed.
*
* 4. Redistribution. You may reproduce and distribute copies of the
* Work or Derivative Works thereof in any medium, with or without
* modifications, and in Source or Object form, provided that You
* meet the following conditions:
*
* (a) You must give any other recipients of the Work or
* Derivative Works a copy of this License; and
*
* (b) You must cause any modified files to carry prominent notices
* stating that You changed the files; and
*
* (c) You must retain, in the Source form of any Derivative Works
* that You distribute, all copyright, patent, trademark, and
* attribution notices from the Source form of the Work,
* excluding those notices that do not pertain to any part of
* the Derivative Works; and
*
* (d) If the Work includes a "NOTICE" text file as part of its
* distribution, then any Derivative Works that You distribute must
* include a readable copy of the attribution notices contained
* within such NOTICE file, excluding those notices that do not
* pertain to any part of the Derivative Works, in at least one
* of the following places: within a NOTICE text file distributed
* as part of the Derivative Works; within the Source form or
* documentation, if provided along with the Derivative Works; or,
* within a display generated by the Derivative Works, if and
* wherever such third-party notices normally appear. The contents
* of the NOTICE file are for informational purposes only and
* do not modify the License. You may add Your own attribution
* notices within Derivative Works that You distribute, alongside
* or as an addendum to the NOTICE text from the Work, provided
* that such additional attribution notices cannot be construed
* as modifying the License.
*
* You may add Your own copyright statement to Your modifications and
* may provide additional or different license terms and conditions
* for use, reproduction, or distribution of Your modifications, or
* for any such Derivative Works as a whole, provided Your use,
* reproduction, and distribution of the Work otherwise complies with
* the conditions stated in this License.
*
* 5. Submission of Contributions. Unless You explicitly state otherwise,
* any Contribution intentionally submitted for inclusion in the Work
* by You to the Licensor shall be under the terms and conditions of
* this License, without any additional terms or conditions.
* Notwithstanding the above, nothing herein shall supersede or modify
* the terms of any separate license agreement you may have executed
* with Licensor regarding such Contributions.
*
* 6. Trademarks. This License does not grant permission to use the trade
* names, trademarks, service marks, or product names of the Licensor,
* except as required for reasonable and customary use in describing the
* origin of the Work and reproducing the content of the NOTICE file.
*
* 7. Disclaimer of Warranty. Unless required by applicable law or
* agreed to in writing, Licensor provides the Work (and each
* Contributor provides its Contributions) on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied, including, without limitation, any warranties or conditions
* of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
* PARTICULAR PURPOSE. You are solely responsible for determining the
* appropriateness of using or redistributing the Work and assume any
* risks associated with Your exercise of permissions under this License.
*
* 8. Limitation of Liability. In no event and under no legal theory,
* whether in tort (including negligence), contract, or otherwise,
* unless required by applicable law (such as deliberate and grossly
* negligent acts) or agreed to in writing, shall any Contributor be
* liable to You for damages, including any direct, indirect, special,
* incidental, or consequential damages of any character arising as a
* result of this License or out of the use or inability to use the
* Work (including but not limited to damages for loss of goodwill,
* work stoppage, computer failure or malfunction, or any and all
* other commercial damages or losses), even if such Contributor
* has been advised of the possibility of such damages.
*
* 9. Accepting Warranty or Additional Liability. While redistributing
* the Work or Derivative Works thereof, You may choose to offer,
* and charge a fee for, acceptance of support, warranty, indemnity,
* or other liability obligations and/or rights consistent with this
* License. However, in accepting such obligations, You may act only
* on Your own behalf and on Your sole responsibility, not on behalf
* of any other Contributor, and only if You agree to indemnify,
* defend, and hold each Contributor harmless for any liability
* incurred by, or claims asserted against, such Contributor by reason
* of your accepting any such warranty or additional liability.
*
* END OF TERMS AND CONDITIONS
*
* APPENDIX: How to apply the Apache License to your work.
*
* To apply the Apache License to your work, attach the following
* boilerplate notice, with the fields enclosed by brackets "{}"
* replaced with your own identifying information. (Don't include
* the brackets!) The text should be enclosed in the appropriate
* comment syntax for the file format. We also recommend that a
* file or class name and description of purpose be included on the
* same "printed page" as the copyright notice for easier
* identification within third-party archives.
*
* Copyright 2014 Edgar Espina
*
* 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 org.jooby;
import com.google.common.base.CaseFormat;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import static java.util.Objects.requireNonNull;
import org.jooby.funzy.Throwing;
import org.jooby.handlers.AssetHandler;
import org.jooby.internal.RouteImpl;
import org.jooby.internal.RouteMatcher;
import org.jooby.internal.RoutePattern;
import org.jooby.internal.RouteSourceImpl;
import org.jooby.internal.SourceProvider;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Routes are a key concept in Jooby. Routes are executed in the same order they are defined.
*
* handlers
*
* There are few type of handlers: {@link Route.Handler}, {@link Route.OneArgHandler}
* {@link Route.ZeroArgHandler} and {@link Route.Filter}. They behave very similar, except that a
* {@link Route.Filter} can decide if the next route handler can be executed or not. For example:
*
*
*
* get("/filter", (req, rsp, chain) {@literal ->} {
* if (someCondition) {
* chain.next(req, rsp);
* } else {
* // respond, throw err, etc...
* }
* });
*
*
* While a {@link Route.Handler} always execute the next handler:
*
*
* get("/path", (req, rsp) {@literal ->} {
* rsp.send("handler");
* });
*
* // filter version
* get("/path", (req, rsp, chain) {@literal ->} {
* rsp.send("handler");
* chain.next(req, rsp);
* });
*
*
* The {@link Route.OneArgHandler} and {@link Route.ZeroArgHandler} offers a functional version of
* generating a response:
*
* {@code
* {
* get("/path", req -> "handler");
*
* get("/path", () -> "handler");
* }
* }
*
* There is no need to call {@link Response#send(Object)}.
*
* path patterns
*
* Jooby supports Ant-style path patterns:
*
*
* Some examples:
*
*
* {@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.jsp} or
* {@code com/txst.html}
* {@code com/*.html} - matches all {@code .html} files in the {@code com} directory
* com/{@literal **}/test.html
- matches all {@code test.html} files underneath the
* {@code com} path
* {@code **}/{@code *} - matches any path at any level.
* {@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.
*
*
* variables
*
* Jooby supports path parameters too:
*
*
* Some examples:
*
*
* /user/{id}
- /user/* and give you access to the id
var.
* /user/:id
- /user/* and give you access to the id
var.
* /user/{id:\\d+}
- /user/[digits] and give you access to the numeric
* id
var.
*
*
* routes semantic
*
* Routes are executed in the order they are defined, for example:
*
*
*
* get("/", (req, rsp) {@literal ->} {
* log.info("first"); // start here and go to second
* });
*
* get("/", (req, rsp) {@literal ->} {
* log.info("second"); // execute after first and go to final
* });
*
* get("/", (req, rsp) {@literal ->} {
* rsp.send("final"); // done!
* });
*
*
* Please note first and second routes are converted to a filter, so previous example is the same
* as:
*
*
* get("/", (req, rsp, chain) {@literal ->} {
* log.info("first"); // start here and go to second
* chain.next(req, rsp);
* });
*
* get("/", (req, rsp, chain) {@literal ->} {
* log.info("second"); // execute after first and go to final
* chain.next(req, rsp);
* });
*
* get("/", (req, rsp) {@literal ->} {
* rsp.send("final"); // done!
* });
*
*
* script route
*
* A script route can be defined using Lambda expressions, like:
*
*
*
* get("/", (request, response) {@literal ->} {
* response.send("Hello Jooby");
* });
*
*
* Due to the use of lambdas a route is a singleton and you should NOT use global variables.
* For example this is a bad practice:
*
*
* List{@literal <}String{@literal >} names = new ArrayList{@literal <>}(); // names produces side effects
* get("/", (req, rsp) {@literal ->} {
* names.add(req.param("name").value();
* // response will be different between calls.
* rsp.send(names);
* });
*
*
* mvc Route
*
* A Mvc Route use annotations to define routes:
*
*
*
* {
* use(MyRoute.class);
* }
*
*
* MyRoute.java:
*
* {@literal @}Path("/")
* public class MyRoute {
*
* {@literal @}GET
* public String hello() {
* return "Hello Jooby";
* }
* }
*
*
* Programming model is quite similar to JAX-RS/Jersey with some minor differences and/or
* simplifications.
*
*
*
* To learn more about Mvc Routes, please check {@link org.jooby.mvc.Path},
* {@link org.jooby.mvc.Produces} {@link org.jooby.mvc.Consumes}.
*
*
* @author edgar
* @since 0.1.0
*/
public interface Route {
/**
* Provides useful information about where the route was defined.
*
* See {@link Definition#source()} and {@link Route#source()}.
*
* @author edgar
* @since 1.0.0.CR4
*/
interface Source {
/**
* There is no source information.
*/
Source BUILTIN = new Source() {
@Override
public int line() {
return -1;
}
@Override
public Optional declaringClass() {
return Optional.empty();
}
@Override
public String toString() {
return "~builtin";
}
};
/**
* @return Line number where the route was defined or -1
when not available.
*/
int line();
/**
* @return Class where the route
*/
@Nonnull
Optional declaringClass();
}
/**
* Converts a route output to something else, see {@link Router#map(Mapper)}.
*
* {@code
* {
* // we got bar.. not foo
* get("/foo", () -> "foo")
* .map(value -> "bar");
*
* // we got foo.. not bar
* get("/bar", () -> "bar")
* .map(value -> "foo");
* }
* }
*
* If you want to apply a single map to several routes:
*
* {@code
* {
* with(() -> {
* get("/foo", () -> "foo");
*
* get("/bar", () -> "bar");
*
* }).map(v -> "foo or bar");
* }
* }
*
* You can apply a {@link Mapper} to specific return type:
*
* {@code
* {
* with(() -> {
* get("/str", () -> "str");
*
* get("/int", () -> 1);
*
* }).map(String v -> "{" + v + "}");
* }
* }
*
* A call to /str
produces {str}
, while /int
just
* 1
.
*
* NOTE : You can apply the map operator to routes that produces an output.
*
* For example, the map operator will be silently ignored here:
*
* {@code
* {
* get("/", (req, rsp) -> {
* rsp.send(...);
* }).map(v -> ..);
* }
* }
*
* @author edgar
* @param Type to map.
*/
interface Mapper {
/**
* Produces a new mapper by combining the two mapper into one.
*
* @param it The first mapper to apply.
* @param next The second mapper to apply.
* @return A new mapper.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Nonnull
static Mapper chain(final Mapper it, final Mapper next) {
return create(it.name() + ">" + next.name(), v -> next.map(it.map(v)));
}
/**
* Creates a new named mapper (just syntax suggar for creating a new mapper).
*
* @param name Mapper's name.
* @param fn Map function.
* @param Value type.
* @return A new mapper.
*/
@Nonnull
static Mapper create(final String name, final Throwing.Function fn) {
return new Route.Mapper() {
@Override
public String name() {
return name;
}
@Override
public Object map(final T value) throws Throwable {
return fn.apply(value);
}
@Override
public String toString() {
return name();
}
};
}
/**
* @return Mapper's name.
*/
@Nonnull
default String name() {
String name = Optional.ofNullable(Strings.emptyToNull(getClass().getSimpleName()))
.orElseGet(() -> {
String classname = getClass().getName();
return classname.substring(classname.lastIndexOf('.') + 1);
});
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, name);
}
/**
* Map the type to something else.
*
* @param value Value to map.
* @return Mapped value.
* @throws Throwable If mapping fails.
*/
@Nonnull
Object map(T value) throws Throwable;
}
/**
* Common route properties, like static and global metadata via attributes, path exclusion,
* produces and consumes types.
*
* @author edgar
* @since 1.0.0.CR
* @param Attribute subtype.
*/
interface Props> {
/**
* Set route attribute. Only primitives, string, class, enum or array of previous types are
* allowed as attributes values.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This instance.
*/
@Nonnull
T attr(String name, Object value);
/**
* Tell jooby what renderer should use to render the output.
*
* @param name A renderer's name.
* @return This instance.
*/
@Nonnull
T renderer(final String name);
/**
* Explicit renderer to use or null
.
*
* @return Explicit renderer to use or null
.
*/
@Nullable
String renderer();
/**
* Set the route name. Route's name, helpful for debugging but also to implement dynamic and
* advanced routing. See {@link Route.Chain#next(String, Request, Response)}
*
*
* @param name A route's name.
* @return This instance.
*/
@Nonnull
T name(final String name);
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This instance.
*/
@Nonnull
default T consumes(final MediaType... consumes) {
return consumes(Arrays.asList(consumes));
}
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This instance.
*/
@Nonnull
default T consumes(final String... consumes) {
return consumes(MediaType.valueOf(consumes));
}
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This instance.
*/
@Nonnull
T consumes(final List consumes);
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This instance.
*/
@Nonnull
default T produces(final MediaType... produces) {
return produces(Arrays.asList(produces));
}
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This instance.
*/
@Nonnull
default T produces(final String... produces) {
return produces(MediaType.valueOf(produces));
}
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This instance.
*/
@Nonnull
T produces(final List produces);
/**
* Excludes one or more path pattern from this route, useful for filter:
*
*
* {
* use("*", req {@literal ->} {
* ...
* }).excludes("/logout");
* }
*
*
* @param excludes A path pattern.
* @return This instance.
*/
@Nonnull
default T excludes(final String... excludes) {
return excludes(Arrays.asList(excludes));
}
/**
* Excludes one or more path pattern from this route, useful for filter:
*
*
* {
* use("*", req {@literal ->} {
* ...
* }).excludes("/logout");
* }
*
*
* @param excludes A path pattern.
* @return This instance.
*/
@Nonnull
T excludes(final List excludes);
@Nonnull
T map(Mapper> mapper);
}
/**
* Group one ore more routes under a base path, see {@link Router#use(String)}.
*
* @author edgar
* @deprecated Replaced by {@link Router#path(String, Runnable)}.
*/
@Deprecated class Group implements Props {
/** List of definitions. */
private List routes = new ArrayList<>();
private String rootPattern;
private String prefix;
private String renderer;
public Group(final String pattern, final String prefix) {
requireNonNull(pattern, "Pattern is required.");
this.rootPattern = pattern;
this.prefix = prefix;
}
public Group(final String pattern) {
this(pattern, null);
}
@Override
public String renderer() {
return renderer;
}
@Override
public Group renderer(final String name) {
this.renderer = name;
return this;
}
public List routes() {
return routes;
}
// ********************************************************************************************
// ALL
// ********************************************************************************************
@Nonnull
public Group all(final String pattern, final Route.Filter filter) {
newRoute("*", pattern, filter);
return this;
}
@Nonnull
public Group all(final String pattern, final Route.Handler handler) {
newRoute("*", pattern, handler);
return this;
}
@Nonnull
public Group all(final String pattern, final Route.OneArgHandler handler) {
newRoute("*", pattern, handler);
return this;
}
@Nonnull
public Group all(final String pattern, final Route.ZeroArgHandler handler) {
newRoute("*", pattern, handler);
return this;
}
@Nonnull
public Group all(final Route.Filter filter) {
newRoute("*", "", filter);
return this;
}
@Nonnull
public Group all(final Route.Handler handler) {
newRoute("*", "", handler);
return this;
}
@Nonnull
public Group all(final Route.OneArgHandler handler) {
newRoute("*", "", handler);
return this;
}
@Nonnull
public Group all(final Route.ZeroArgHandler handler) {
newRoute("*", "", handler);
return this;
}
// ********************************************************************************************
// GET
// ********************************************************************************************
@Nonnull
public Group get(final String pattern, final Route.Filter filter) {
newRoute(GET, pattern, filter);
return this;
}
@Nonnull
public Group get(final String pattern, final Route.Handler handler) {
newRoute(GET, pattern, handler);
return this;
}
@Nonnull
public Group get(final String pattern, final Route.OneArgHandler handler) {
newRoute(GET, pattern, handler);
return this;
}
@Nonnull
public Group get(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(GET, pattern, handler);
return this;
}
@Nonnull
public Group get(final Route.Filter filter) {
newRoute(GET, "", filter);
return this;
}
@Nonnull
public Group get(final Route.Handler handler) {
newRoute(GET, "", handler);
return this;
}
@Nonnull
public Group get(final Route.OneArgHandler handler) {
newRoute(GET, "", handler);
return this;
}
@Nonnull
public Group get(final Route.ZeroArgHandler handler) {
newRoute(GET, "", handler);
return this;
}
// ********************************************************************************************
// POST
// ********************************************************************************************
@Nonnull
public Group post(final String pattern, final Route.Filter filter) {
newRoute(POST, pattern, filter);
return this;
}
@Nonnull
public Group post(final String pattern, final Route.Handler handler) {
newRoute(POST, pattern, handler);
return this;
}
@Nonnull
public Group post(final String pattern, final Route.OneArgHandler handler) {
newRoute(POST, pattern, handler);
return this;
}
@Nonnull
public Group post(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(POST, pattern, handler);
return this;
}
@Nonnull
public Group post(final Route.Filter filter) {
newRoute(POST, "", filter);
return this;
}
@Nonnull
public Group post(final Route.Handler handler) {
newRoute(POST, "", handler);
return this;
}
@Nonnull
public Group post(final Route.OneArgHandler handler) {
newRoute(POST, "", handler);
return this;
}
@Nonnull
public Group post(final Route.ZeroArgHandler handler) {
newRoute(POST, "", handler);
return this;
}
// ********************************************************************************************
// PUT
// ********************************************************************************************
@Nonnull
public Group put(final String pattern, final Route.Filter filter) {
newRoute(PUT, pattern, filter);
return this;
}
@Nonnull
public Group put(final String pattern, final Route.Handler handler) {
newRoute(PUT, pattern, handler);
return this;
}
@Nonnull
public Group put(final String pattern, final Route.OneArgHandler handler) {
newRoute(PUT, pattern, handler);
return this;
}
@Nonnull
public Group put(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(PUT, pattern, handler);
return this;
}
@Nonnull
public Group put(final Route.Filter filter) {
newRoute(PUT, "", filter);
return this;
}
@Nonnull
public Group put(final Route.Handler handler) {
newRoute(PUT, "", handler);
return this;
}
@Nonnull
public Group put(final Route.OneArgHandler handler) {
newRoute(PUT, "", handler);
return this;
}
@Nonnull
public Group put(final Route.ZeroArgHandler handler) {
newRoute(PUT, "", handler);
return this;
}
// ********************************************************************************************
// DELETE
// ********************************************************************************************
@Nonnull
public Group delete(final String pattern, final Route.Filter filter) {
newRoute(DELETE, pattern, filter);
return this;
}
@Nonnull
public Group delete(final String pattern, final Route.Handler handler) {
newRoute(DELETE, pattern, handler);
return this;
}
@Nonnull
public Group delete(final String pattern, final Route.OneArgHandler handler) {
newRoute(DELETE, pattern, handler);
return this;
}
@Nonnull
public Group delete(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(DELETE, pattern, handler);
return this;
}
@Nonnull
public Group delete(final Route.Filter filter) {
newRoute(DELETE, "", filter);
return this;
}
@Nonnull
public Group delete(final Route.Handler handler) {
newRoute(DELETE, "", handler);
return this;
}
@Nonnull
public Group delete(final Route.OneArgHandler handler) {
newRoute(DELETE, "", handler);
return this;
}
@Nonnull
public Group delete(final Route.ZeroArgHandler handler) {
newRoute(DELETE, "", handler);
return this;
}
// ********************************************************************************************
// PATCH
// ********************************************************************************************
@Nonnull
public Group patch(final String pattern, final Route.Filter filter) {
newRoute(PATCH, pattern, filter);
return this;
}
@Nonnull
public Group patch(final String pattern, final Route.Handler handler) {
newRoute(PATCH, pattern, handler);
return this;
}
@Nonnull
public Group patch(final String pattern, final Route.OneArgHandler handler) {
newRoute(PATCH, pattern, handler);
return this;
}
@Nonnull
public Group patch(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(PATCH, pattern, handler);
return this;
}
@Nonnull
public Group patch(final Route.Filter filter) {
newRoute(PATCH, "", filter);
return this;
}
@Nonnull
public Group patch(final Route.Handler handler) {
newRoute(PATCH, "", handler);
return this;
}
@Nonnull
public Group patch(final Route.OneArgHandler handler) {
newRoute(PATCH, "", handler);
return this;
}
@Nonnull
public Group patch(final Route.ZeroArgHandler handler) {
newRoute(PATCH, "", handler);
return this;
}
/**
* Set the route name to the whole collection.
*
* @param name Name to use/set.
* @return This instance.
*/
@Override
@Nonnull
public Group name(final String name) {
for (Definition definition : routes) {
if (prefix != null) {
definition.name(prefix + "/" + name);
} else {
definition.name(name);
}
}
return this;
}
@Override
@Nonnull
public Group consumes(final List types) {
for (Definition definition : routes) {
definition.consumes(types);
}
return this;
}
@Override
@Nonnull
public Group produces(final List types) {
for (Definition definition : routes) {
definition.produces(types);
}
return this;
}
@Override
@Nonnull
public Group attr(final String name, final Object value) {
for (Definition definition : routes) {
definition.attr(name, value);
}
return this;
}
@Override
@Nonnull
public Group excludes(final List excludes) {
for (Definition definition : routes) {
definition.excludes(excludes);
}
return this;
}
@Override
@Nonnull
public Group map(final Mapper> mapper) {
for (Definition definition : routes) {
definition.map(mapper);
}
return this;
}
private void newRoute(final String method, final String pattern,
final Route.Filter filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final String method, final String pattern,
final Route.Handler filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final String method, final String pattern,
final Route.OneArgHandler filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final String method, final String pattern,
final Route.ZeroArgHandler filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final Route.Definition route) {
if (prefix != null) {
route.name(prefix);
}
routes.add(route);
}
}
;
/**
* Collection of {@link Route.Props} useful for registering/setting route options at once.
*
* See {@link Router#get(String, String, String, OneArgHandler)} and variants.
*
* @author edgar
* @since 0.5.0
*/
@SuppressWarnings({"unchecked", "rawtypes"}) class Collection implements Props {
/** List of definitions. */
private final Route.Props[] routes;
/**
* Creates a new collection of route definitions.
*
* @param definitions Collection of route definitions.
*/
public Collection(final Route.Props... definitions) {
this.routes = requireNonNull(definitions, "Route definitions are required.");
}
@Override
public Collection name(final String name) {
for (Props definition : routes) {
definition.name(name);
}
return this;
}
@Override
public String renderer() {
return routes[0].renderer();
}
@Override
public Collection renderer(final String name) {
for (Props definition : routes) {
definition.renderer(name);
}
return this;
}
@Override
public Collection consumes(final List types) {
for (Props definition : routes) {
definition.consumes(types);
}
return this;
}
@Override
public Collection produces(final List types) {
for (Props definition : routes) {
definition.produces(types);
}
return this;
}
@Override
public Collection attr(final String name, final Object value) {
for (Props definition : routes) {
definition.attr(name, value);
}
return this;
}
@Override
public Collection excludes(final List excludes) {
for (Props definition : routes) {
definition.excludes(excludes);
}
return this;
}
@Override
public Collection map(final Mapper> mapper) {
for (Props route : routes) {
route.map(mapper);
}
return this;
}
}
/**
* DSL for customize routes.
*
*
* Some examples:
*
*
*
* public class MyApp extends Jooby {
* {
* get("/", () {@literal ->} "GET");
*
* post("/", req {@literal ->} "POST");
*
* put("/", (req, rsp) {@literal ->} rsp.send("PUT"));
* }
* }
*
*
* Setting what a route can consumes
*
*
* public class MyApp extends Jooby {
* {
* post("/", (req, resp) {@literal ->} resp.send("POST"))
* .consumes(MediaType.json);
* }
* }
*
*
* Setting what a route can produces
*
*
* public class MyApp extends Jooby {
* {
* post("/", (req, resp) {@literal ->} resp.send("POST"))
* .produces(MediaType.json);
* }
* }
*
*
* Adding a name
*
*
* public class MyApp extends Jooby {
* {
* post("/", (req, resp) {@literal ->} resp.send("POST"))
* .name("My Root");
* }
* }
*
*
* @author edgar
* @since 0.1.0
*/
class Definition implements Props {
/**
* Route's name.
*/
private String name = "/anonymous";
/**
* A route pattern.
*/
private RoutePattern cpattern;
/**
* The target route.
*/
private Filter filter;
/**
* Defines the media types that the methods of a resource class or can accept. Default is:
* {@code *}/{@code *}.
*/
private List consumes = MediaType.ALL;
/**
* Defines the media types that the methods of a resource class or can produces. Default is:
* {@code *}/{@code *}.
*/
private List produces = MediaType.ALL;
/**
* A HTTP verb or *
.
*/
private String method;
/**
* A path pattern.
*/
private String pattern;
private List excludes = Collections.emptyList();
private Map attributes = ImmutableMap.of();
private Mapper> mapper;
private int line;
private String declaringClass;
String prefix;
private String renderer;
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or *
.
* @param pattern A path pattern.
* @param handler A route handler.
*/
public Definition(final String verb, final String pattern,
final Route.Handler handler) {
this(verb, pattern, (Route.Filter) handler);
}
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or *
.
* @param pattern A path pattern.
* @param handler A route handler.
* @param caseSensitiveRouting Configure case for routing algorithm.
*/
public Definition(final String verb, final String pattern,
final Route.Handler handler, boolean caseSensitiveRouting) {
this(verb, pattern, (Route.Filter) handler, caseSensitiveRouting);
}
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or *
.
* @param pattern A path pattern.
* @param handler A route handler.
*/
public Definition(final String verb, final String pattern,
final Route.OneArgHandler handler) {
this(verb, pattern, (Route.Filter) handler);
}
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or *
.
* @param pattern A path pattern.
* @param handler A route handler.
*/
public Definition(final String verb, final String pattern,
final Route.ZeroArgHandler handler) {
this(verb, pattern, (Route.Filter) handler);
}
/**
* Creates a new route definition.
*
* @param method A HTTP verb or *
.
* @param pattern A path pattern.
* @param filter A callback to execute.
*/
public Definition(final String method, final String pattern, final Filter filter) {
this(method, pattern, filter, true);
}
/**
* Creates a new route definition.
*
* @param method A HTTP verb or *
.
* @param pattern A path pattern.
* @param filter A callback to execute.
* @param caseSensitiveRouting Configure case for routing algorithm.
*/
public Definition(final String method, final String pattern,
final Filter filter, boolean caseSensitiveRouting) {
requireNonNull(pattern, "A route path is required.");
requireNonNull(filter, "A filter is required.");
this.method = method.toUpperCase();
this.cpattern = new RoutePattern(method, pattern, !caseSensitiveRouting);
// normalized pattern
this.pattern = cpattern.pattern();
this.filter = filter;
SourceProvider.INSTANCE.get().ifPresent(source -> {
this.line = source.getLineNumber();
this.declaringClass = source.getClassName();
});
}
/**
* Path Patterns
*
* Jooby supports Ant-style path patterns:
*
*
* Some examples:
*
*
* {@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.jsp} or
* {@code com/txst.html}
* {@code com/*.html} - matches all {@code .html} files in the {@code com} directory
* com/{@literal **}/test.html
- matches all {@code test.html} files underneath
* the {@code com} path
* {@code **}/{@code *} - matches any path at any level.
* {@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.
*
*
* Variables
*
* Jooby supports path parameters too:
*
*
* Some examples:
*
*
* /user/{id}
- /user/* and give you access to the id
var.
* /user/:id
- /user/* and give you access to the id
var.
* /user/{id:\\d+}
- /user/[digits] and give you access to the numeric
* id
var.
*
*
* @return A path pattern.
*/
@Nonnull
public String pattern() {
return pattern;
}
@Nullable
public String renderer() {
return renderer;
}
@Override
public Definition renderer(final String name) {
this.renderer = name;
return this;
}
/**
* @return List of path variables (if any).
*/
@Nonnull
public List vars() {
return cpattern.vars();
}
/**
* Indicates if the {@link #pattern()} contains a glob charecter, like ?
,
* *
or **
.
*
* @return Indicates if the {@link #pattern()} contains a glob charecter, like ?
,
* *
or **
.
*/
@Nonnull
public boolean glob() {
return cpattern.glob();
}
/**
* Source information (where the route was defined).
*
* @return Source information (where the route was defined).
*/
@Nonnull
public Route.Source source() {
return new RouteSourceImpl(declaringClass, line);
}
/**
* Recreate a route path and apply the given variables.
*
* @param vars Path variables.
* @return A route pattern.
*/
@Nonnull
public String reverse(final Map vars) {
return cpattern.reverse(vars);
}
/**
* Recreate a route path and apply the given variables.
*
* @param values Path variable values.
* @return A route pattern.
*/
@Nonnull
public String reverse(final Object... values) {
return cpattern.reverse(values);
}
@Override
@Nonnull
public Definition attr(final String name, final Object value) {
requireNonNull(name, "Attribute name is required.");
requireNonNull(value, "Attribute value is required.");
if (valid(value)) {
attributes = ImmutableMap.builder()
.putAll(attributes)
.put(name, value)
.build();
}
return this;
}
private boolean valid(final Object value) {
if (Primitives.isWrapperType(Primitives.wrap(value.getClass()))) {
return true;
}
if (value instanceof String || value instanceof Enum || value instanceof Class) {
return true;
}
if (value.getClass().isArray()) {
return valid(Array.get(value, 0));
}
if (value instanceof Map && ((Map) value).size() > 0) {
Map.Entry e = (Map.Entry) ((Map) value).entrySet().iterator().next();
return valid(e.getKey()) && valid(e.getValue());
}
return false;
}
/**
* Get an attribute by name.
*
* @param name Attribute's name.
* @param Attribute's type.
* @return Attribute's value or null
.
*/
@SuppressWarnings("unchecked")
@Nonnull
public T attr(final String name) {
return (T) attributes.get(name);
}
/**
* @return A read only view of attributes.
*/
@Nonnull
public Map attributes() {
return attributes;
}
/**
* Test if the route matches the given verb, path, content type and accept header.
*
* @param method A HTTP verb.
* @param path Current HTTP path.
* @param contentType The Content-Type
header.
* @param accept The Accept
header.
* @return A route or an empty optional.
*/
@Nonnull
public Optional matches(final String method,
final String path, final MediaType contentType,
final List accept) {
String fpath = method + path;
if (excludes.size() > 0 && excludes(fpath)) {
return Optional.empty();
}
RouteMatcher matcher = cpattern.matcher(fpath);
if (matcher.matches()) {
List result = MediaType.matcher(accept).filter(this.produces);
if (result.size() > 0 && canConsume(contentType)) {
// keep accept when */*
List produces = result.size() == 1 && result.get(0).name().equals("*/*")
? accept : this.produces;
return Optional
.of(asRoute(method, matcher, produces, new RouteSourceImpl(declaringClass, line)));
}
}
return Optional.empty();
}
/**
* @return HTTP method or *
.
*/
@Nonnull
public String method() {
return method;
}
/**
* @return Handler behind this route.
*/
@Nonnull
public Route.Filter filter() {
return filter;
}
/**
* Route's name, helpful for debugging but also to implement dynamic and advanced routing. See
* {@link Route.Chain#next(String, Request, Response)}
*
* @return Route name. Default is: anonymous
.
*/
@Nonnull
public String name() {
return name;
}
/**
* Set the route name. Route's name, helpful for debugging but also to implement dynamic and
* advanced routing. See {@link Route.Chain#next(String, Request, Response)}
*
*
* @param name A route's name.
* @return This definition.
*/
@Override
@Nonnull
public Definition name(final String name) {
checkArgument(!Strings.isNullOrEmpty(name), "A route's name is required.");
this.name = normalize(prefix != null ? prefix + "/" + name : name);
return this;
}
/**
* Test if the route definition can consume a media type.
*
* @param type A media type to test.
* @return True, if the route can consume the given media type.
*/
public boolean canConsume(final MediaType type) {
return MediaType.matcher(Arrays.asList(type)).matches(consumes);
}
/**
* Test if the route definition can consume a media type.
*
* @param type A media type to test.
* @return True, if the route can consume the given media type.
*/
public boolean canConsume(final String type) {
return MediaType.matcher(MediaType.valueOf(type)).matches(consumes);
}
/**
* Test if the route definition can consume a media type.
*
* @param types A media types to test.
* @return True, if the route can produces the given media type.
*/
public boolean canProduce(final List types) {
return MediaType.matcher(types).matches(produces);
}
/**
* Test if the route definition can consume a media type.
*
* @param types A media types to test.
* @return True, if the route can produces the given media type.
*/
public boolean canProduce(final MediaType... types) {
return canProduce(Arrays.asList(types));
}
/**
* Test if the route definition can consume a media type.
*
* @param types A media types to test.
* @return True, if the route can produces the given media type.
*/
public boolean canProduce(final String... types) {
return canProduce(MediaType.valueOf(types));
}
@Override
public Definition consumes(final List types) {
checkArgument(types != null && types.size() > 0, "Consumes types are required");
if (types.size() > 1) {
this.consumes = Lists.newLinkedList(types);
Collections.sort(this.consumes);
} else {
this.consumes = ImmutableList.of(types.get(0));
}
return this;
}
@Override
public Definition produces(final List types) {
checkArgument(types != null && types.size() > 0, "Produces types are required");
if (types.size() > 1) {
this.produces = Lists.newLinkedList(types);
Collections.sort(this.produces);
} else {
this.produces = ImmutableList.of(types.get(0));
}
return this;
}
@Override
public Definition excludes(final List excludes) {
this.excludes = excludes.stream()
.map(it -> new RoutePattern(method, it))
.collect(Collectors.toList());
return this;
}
/**
* @return List of exclusion filters (if any).
*/
@Nonnull
public List excludes() {
return excludes.stream().map(r -> r.pattern()).collect(Collectors.toList());
}
private boolean excludes(final String path) {
for (RoutePattern pattern : excludes) {
if (pattern.matcher(path).matches()) {
return true;
}
}
return false;
}
/**
* @return All the types this route can consumes.
*/
@Nonnull
public List consumes() {
return Collections.unmodifiableList(this.consumes);
}
/**
* @return All the types this route can produces.
*/
@Nonnull
public List produces() {
return Collections.unmodifiableList(this.produces);
}
@Override
@Nonnull
public Definition map(final Mapper> mapper) {
this.mapper = requireNonNull(mapper, "Mapper is required.");
return this;
}
/**
* Set the line where this route is defined.
*
* @param line Line number.
* @return This instance.
*/
@Nonnull
public Definition line(final int line) {
this.line = line;
return this;
}
/**
* Set the class where this route is defined.
*
* @param declaringClass A source class.
* @return This instance.
*/
@Nonnull
public Definition declaringClass(final String declaringClass) {
this.declaringClass = declaringClass;
return this;
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append(method()).append(" ").append(pattern()).append("\n");
buffer.append(" name: ").append(name()).append("\n");
buffer.append(" excludes: ").append(excludes).append("\n");
buffer.append(" consumes: ").append(consumes()).append("\n");
buffer.append(" produces: ").append(produces()).append("\n");
return buffer.toString();
}
/**
* Creates a new route.
*
* @param method A HTTP verb.
* @param matcher A route matcher.
* @param produces List of produces types.
* @param source Route source.
* @return A new route.
*/
private Route asRoute(final String method, final RouteMatcher matcher,
final List produces, final Route.Source source) {
return new RouteImpl(filter, this, method, matcher.path(), produces,
matcher.vars(), mapper, source);
}
}
/**
* A forwarding route.
*
* @author edgar
* @since 0.1.0
*/
class Forwarding implements Route {
/**
* Target route.
*/
private final Route route;
/**
* Creates a new {@link Forwarding} route.
*
* @param route A target route.
*/
public Forwarding(final Route route) {
if (route == null) {
throw new NullPointerException("Route required");
}
this.route = route;
}
@Override
public String renderer() {
return route.renderer();
}
@Override
public String path() {
return route.path();
}
@Override
public String method() {
return route.method();
}
@Override
public String pattern() {
return route.pattern();
}
@Override
public String name() {
return route.name();
}
@Override
public Map vars() {
return route.vars();
}
@Override
public List consumes() {
return route.consumes();
}
@Override
public List produces() {
return route.produces();
}
@Override
public Map attributes() {
return route.attributes();
}
@Override
public T attr(final String name) {
return route.attr(name);
}
@Override
public boolean glob() {
return route.glob();
}
@Override
public String reverse(final Map vars) {
return route.reverse(vars);
}
@Override
public String reverse(final Object... values) {
return route.reverse(values);
}
@Override
public Source source() {
return route.source();
}
@Override
public String print() {
return route.print();
}
@Override
public String print(final int indent) {
return route.print(indent);
}
@Override
public String toString() {
return route.toString();
}
/**
* Find a target route.
*
* @param route A route to check.
* @return A target route.
*/
public static Route unwrap(final Route route) {
Route root = route;
while (root instanceof Forwarding) {
root = ((Forwarding) root).route;
}
return root;
}
}
/**
* The most advanced route handler which let you decided if the next route handler in the chain
* can be executed or not. Example of filters are:
*
*
* Auth handler example:
*
*
*
* String token = req.header("token").value();
* if (token != null) {
* // validate token...
* if (valid(token)) {
* chain.next(req, rsp);
* }
* } else {
* rsp.status(403);
* }
*
*
*
* Logging/Around handler example:
*
*
*
* long start = System.currentTimeMillis();
* chain.next(req, rsp);
* long end = System.currentTimeMillis();
* log.info("Request: {} took {}ms", req.path(), end - start);
*
*
* NOTE: Don't forget to call {@link Route.Chain#next(Request, Response)} if next route handler
* need to be executed.
*
* @author edgar
* @since 0.1.0
*/
public interface Filter {
/**
* The handle
method of the Filter is called by the server each time a
* request/response pair is passed through the chain due to a client request for a resource at
* the end of the chain.
* The {@link Route.Chain} passed in to this method allows the Filter to pass on the request and
* response to the next entity in the chain.
*
*
* A typical implementation of this method would follow the following pattern:
*
*
* Examine the request
* Optionally wrap the request object with a custom implementation to filter content or
* headers for input filtering
* Optionally wrap the response object with a custom implementation to filter content or
* headers for output filtering
*
*
* Either invoke the next entity in the chain using the {@link Route.Chain}
* object (chain.next(req, rsp)
),
* or not pass on the request/response pair to the next entity in the
* filter chain to block the request processing
*
* Directly set headers on the response after invocation of the next entity in the filter
* chain.
*
*
* @param req A HTTP request.
* @param rsp A HTTP response.
* @param chain A route chain.
* @throws Throwable If something goes wrong.
*/
void handle(Request req, Response rsp, Route.Chain chain) throws Throwable;
}
/**
* Allow to customize an asset handler.
*
* @author edgar
*/
class AssetDefinition extends Definition {
private Boolean etag;
private String cdn;
private Object maxAge;
private Boolean lastModifiedSince;
private Integer statusCode;
/**
* Creates a new route definition.
*
* @param method A HTTP verb or *
.
* @param pattern A path pattern.
* @param handler A callback to execute.
* @param caseSensitiveRouting Configure case for routing algorithm.
*/
public AssetDefinition(final String method, final String pattern,
final Route.Filter handler, boolean caseSensitiveRouting) {
super(method, pattern, handler, caseSensitiveRouting);
}
@Nonnull
@Override
public AssetHandler filter() {
return (AssetHandler) super.filter();
}
/**
* Indicates what to do when an asset is missing (not resolved). Default action is to resolve them
* as 404 (NOT FOUND)
request.
*
* If you specify a status code <= 0, missing assets are ignored and the next handler on pipeline
* will be executed.
*
* @param statusCode HTTP code or 0.
* @return This route definition.
*/
public AssetDefinition onMissing(final int statusCode) {
if (this.statusCode == null) {
filter().onMissing(statusCode);
this.statusCode = statusCode;
}
return this;
}
/**
* @param etag Turn on/off etag support.
* @return This route definition.
*/
public AssetDefinition etag(final boolean etag) {
if (this.etag == null) {
filter().etag(etag);
this.etag = etag;
}
return this;
}
/**
* @param enabled Turn on/off last modified support.
* @return This route definition.
*/
public AssetDefinition lastModified(final boolean enabled) {
if (this.lastModifiedSince == null) {
filter().lastModified(enabled);
this.lastModifiedSince = enabled;
}
return this;
}
/**
* @param cdn If set, every resolved asset will be serve from it.
* @return This route definition.
*/
public AssetDefinition cdn(final String cdn) {
if (this.cdn == null) {
filter().cdn(cdn);
this.cdn = cdn;
}
return this;
}
/**
* @param maxAge Set the cache header max-age value.
* @return This route definition.
*/
public AssetDefinition maxAge(final Duration maxAge) {
if (this.maxAge == null) {
filter().maxAge(maxAge);
this.maxAge = maxAge;
}
return this;
}
/**
* @param maxAge Set the cache header max-age value in seconds.
* @return This route definition.
*/
public AssetDefinition maxAge(final long maxAge) {
if (this.maxAge == null) {
filter().maxAge(maxAge);
this.maxAge = maxAge;
}
return this;
}
/**
* Parse value as {@link Duration}. If the value is already a number then it uses as seconds.
* Otherwise, it parse expressions like: 8m, 1h, 365d, etc...
*
* @param maxAge Set the cache header max-age value in seconds.
* @return This route definition.
*/
public AssetDefinition maxAge(final String maxAge) {
if (this.maxAge == null) {
filter().maxAge(maxAge);
this.maxAge = maxAge;
}
return this;
}
}
/**
* A route handler that always call {@link Chain#next(Request, Response)}.
*
*
* public class MyApp extends Jooby {
* {
* get("/", (req, rsp) {@literal ->} rsp.send("Hello"));
* }
* }
*
*
* @author edgar
* @since 0.1.0
*/
interface Handler extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Route.Chain chain)
throws Throwable {
handle(req, rsp);
chain.next(req, rsp);
}
/**
* Callback method for a HTTP request.
*
* @param req A HTTP request.
* @param rsp A HTTP response.
* @throws Throwable If something goes wrong. The exception will processed by Jooby.
*/
void handle(Request req, Response rsp) throws Throwable;
}
/**
* A handler for a MVC route, it extends {@link Handler} by adding a reference to the method
* and class behind this route.
*
* @author edgar
* @since 0.6.2
*/
interface MethodHandler extends Handler {
/**
* Target method.
*
* @return Target method.
*/
@Nonnull
Method method();
/**
* Target class.
*
* @return Target class.
*/
@Nonnull
Class> implementingClass();
}
/**
* A functional route handler that use the return value as HTTP response.
*
*
* {
* get("/",(req {@literal ->} "Hello");
* }
*
*
* @author edgar
* @since 0.1.1
*/
interface OneArgHandler extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Route.Chain chain)
throws Throwable {
Object result = handle(req);
rsp.send(result);
chain.next(req, rsp);
}
/**
* Callback method for a HTTP request.
*
* @param req A HTTP request.
* @return Message to send.
* @throws Throwable If something goes wrong. The exception will processed by Jooby.
*/
Object handle(Request req) throws Throwable;
}
/**
* A functional handler that use the return value as HTTP response.
*
*
* public class MyApp extends Jooby {
* {
* get("/", () {@literal ->} "Hello");
* }
* }
*
*
* @author edgar
* @since 0.1.1
*/
interface ZeroArgHandler extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Route.Chain chain)
throws Throwable {
Object result = handle();
rsp.send(result);
chain.next(req, rsp);
}
/**
* Callback method for a HTTP request.
*
* @return Message to send.
* @throws Throwable If something goes wrong. The exception will processed by Jooby.
*/
Object handle() throws Throwable;
}
/**
* before
*
* Allows for customized handler execution chains. It will be invoked before the actual handler.
*
* {@code
* {
* before((req, rsp) -> {
* // your code goes here
* });
* }
* }
*
* You are allowed to modify the request and response objects.
*
* Please note that the before
handler is just syntax sugar for {@link Route.Filter}.
* For example, the before
handler was implemented as:
*
* {@code
* {
* use("*", "*", (req, rsp, chain) -> {
* before(req, rsp);
* // your code goes here
* chain.next(req, rsp);
* });
* }
* }
*
* A before
handler must to be registered before the actual handler you want to
* intercept.
*
* {@code
* {
* before((req, rsp) -> {
* // your code goes here
* });
*
* get("/path", req -> {
* // your code goes here
* return ...;
* });
* }
* }
*
* If you reverse the order then it won't work.
*
*
* Remember : routes are executed in the order they are defined and the pipeline
* is executed as long you don't generate a response.
*
*
* @author edgar
* @since 1.0.0.CR
*/
interface Before extends Route.Filter {
@Override
default void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
handle(req, rsp);
chain.next(req, rsp);
}
/**
* Allows for customized handler execution chains. It will be invoked before the actual handler.
*
* @param req Request.
* @param rsp Response
* @throws Throwable If something goes wrong.
*/
void handle(Request req, Response rsp) throws Throwable;
}
/**
* after
*
* Allows for customized response before send it. It will be invoked at the time a response need
* to be send.
*
* {@code
* {
* after("GET", "*", (req, rsp, result) -> {
* // your code goes here
* return result;
* });
* }
* }
*
* You are allowed to modify the request, response and result objects. The handler returns a
* {@link Result} which can be the same or an entirely new {@link Result}.
*
* Please note that the after
handler is just syntax sugar for
* {@link Route.Filter}.
* For example, the after
handler was implemented as:
*
* {@code
* {
* use("GET", "*", (req, rsp, chain) -> {
* chain.next(req, new Response.Forwarding(rsp) {
* public void send(Result result) {
* rsp.send(after(req, rsp, result);
* }
* });
* });
* }
* }
*
* Due after
is implemented by wrapping the {@link Response} object. A
* after
handler must to be registered before the actual handler you want to
* intercept.
*
* {@code
* {
* after("GET", "/path", (req, rsp, result) -> {
* // your code goes here
* return result;
* });
*
* get("/path", req -> {
* return "hello";
* });
* }
* }
*
* If you reverse the order then it won't work.
*
*
* Remember : routes are executed in the order they are defined and the pipeline
* is executed as long you don't generate a response.
*
*
* @author edgar
* @since 1.0.0.CR
*/
interface After extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
rsp.after(this);
chain.next(req, rsp);
}
/**
* Allows for customized response before send it. It will be invoked at the time a response need
* to be send.
*
* @param req Request.
* @param rsp Response
* @param result Result.
* @return Same or new result.
* @throws Exception If something goes wrong.
*/
Result handle(Request req, Response rsp, Result result) throws Exception;
}
/**
* complete
*
* Allows for log and cleanup a request. It will be invoked after we send a response.
*
* {@code
* {
* complete((req, rsp, cause) -> {
* // your code goes here
* });
* }
* }
*
* You are NOT allowed to modify the request and response objects. The cause
is an
* {@link Optional} with a {@link Throwable} useful to identify problems.
*
* The goal of the complete
handler is to probably cleanup request object and log
* responses.
*
* Please note that the complete
handler is just syntax sugar for
* {@link Route.Filter}.
* For example, the complete
handler was implemented as:
*
* {@code
* {
* use("*", "*", (req, rsp, chain) -> {
* Optional err = Optional.empty();
* try {
* chain.next(req, rsp);
* } catch (Throwable cause) {
* err = Optional.of(cause);
* } finally {
* complete(req, rsp, err);
* }
* });
* }
* }
*
* An complete
handler must to be registered before the actual handler you want to
* intercept.
*
* {@code
* {
* complete((req, rsp, cause) -> {
* // your code goes here
* });
*
* get(req -> {
* return "hello";
* });
* }
* }
*
* If you reverse the order then it won't work.
*
*
* Remember : routes are executed in the order they are defined and the pipeline
* is executed as long you don't generate a response.
*
*
* example
*
* Suppose you have a transactional resource, like a database connection. The next example shows
* you how to implement a simple and effective transaction-per-request
pattern:
*
*
* {@code
* {
* // start transaction
* before((req, rsp) -> {
* DataSource ds = req.require(DataSource.class);
* Connection connection = ds.getConnection();
* Transaction trx = connection.getTransaction();
* trx.begin();
* req.set("connection", connection);
* return true;
* });
*
* // commit/rollback transaction
* complete((req, rsp, cause) -> {
* // unbind connection from request
* try(Connection connection = req.unset("connection").get()) {
* Transaction trx = connection.getTransaction();
* if (cause.ifPresent()) {
* trx.rollback();
* } else {
* trx.commit();
* }
* }
* });
*
* // your transactional routes goes here
* get("/api/something", req -> {
* Connection connection = req.get("connection");
* // work with connection
* });
* }
* }
*
* @author edgar
* @since 1.0.0.CR
*/
interface Complete extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
rsp.complete(this);
chain.next(req, rsp);
}
/**
* Allows for log and cleanup a request. It will be invoked after we send a response.
*
* @param req Request.
* @param rsp Response
* @param cause Empty optional on success. Otherwise, it contains the exception.
*/
void handle(Request req, Response rsp, Optional cause);
}
/**
* Chain of routes to be executed. It invokes the next route in the chain.
*
* @author edgar
* @since 0.1.0
*/
interface Chain {
/**
* Invokes the next route in the chain where {@link Route#name()} starts with the given prefix.
*
* @param prefix Iterates over the route chain and keep routes that start with the given prefix.
* @param req A HTTP request.
* @param rsp A HTTP response.
* @throws Throwable If invocation goes wrong.
*/
void next(@Nullable String prefix, Request req, Response rsp) throws Throwable;
/**
* Invokes the next route in the chain.
*
* @param req A HTTP request.
* @param rsp A HTTP response.
* @throws Throwable If invocation goes wrong.
*/
default void next(final Request req, final Response rsp) throws Throwable {
next(null, req, rsp);
}
/**
* All the pending/next routes from pipeline. Example:
*
* {@code
* use("*", (req, rsp, chain) -> {
* List routes = chain.routes();
* assertEquals(2, routes.size());
* assertEquals("/r2", routes.get(0).name());
* assertEquals("/r3", routes.get(1).name());
* assertEquals("/786/:id", routes.get(routes.size() - 1).pattern());
*
* chain.next(req, rsp);
* }).name("r1");
*
* use("/786/**", (req, rsp, chain) -> {
* List routes = chain.routes();
* assertEquals(1, routes.size());
* assertEquals("/r3", routes.get(0).name());
* assertEquals("/786/:id", routes.get(routes.size() - 1).pattern());
* chain.next(req, rsp);
* }).name("r2");
*
* get("/786/:id", req -> {
* return req.param("id").value();
* }).name("r3");
* }
*
* @return Next routes in the pipeline or empty list.
*/
List routes();
}
/** Route key. */
Key> KEY = Key.get(new TypeLiteral>() {
});
char OUT_OF_PATH = '\u200B';
String GET = "GET";
String POST = "POST";
String PUT = "PUT";
String DELETE = "DELETE";
String PATCH = "PATCH";
String HEAD = "HEAD";
String CONNECT = "CONNECT";
String OPTIONS = "OPTIONS";
String TRACE = "TRACE";
/**
* Well known HTTP methods.
*/
List METHODS = ImmutableList.builder()
.add(GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
CONNECT,
OPTIONS,
TRACE)
.build();
/**
* @return Current request path.
*/
@Nonnull
String path();
/**
* @return Current HTTP method.
*/
@Nonnull
String method();
/**
* @return The currently matched pattern.
*/
@Nonnull
String pattern();
/**
* Route's name, helpful for debugging but also to implement dynamic and advanced routing. See
* {@link Route.Chain#next(String, Request, Response)}
*
* @return Route name, defaults to "anonymous"
*/
@Nonnull
String name();
/**
* Path variables, either named or by index (capturing group).
*
*
* /path/:var
*
*
* Variable var
is accessible by name: var
or index: 0
.
*
* @return The currently matched path variables (if any).
*/
@Nonnull
Map vars();
/**
* @return List all the types this route can consumes, defaults is: {@code * / *}.
*/
@Nonnull
List consumes();
/**
* @return List all the types this route can produces, defaults is: {@code * / *}.
*/
@Nonnull
List produces();
/**
* True, when route's name starts with the given prefix. Useful for dynamic routing. See
* {@link Route.Chain#next(String, Request, Response)}
*
* @param prefix Prefix to check for.
* @return True, when route's name starts with the given prefix.
*/
default boolean apply(final String prefix) {
return name().startsWith(prefix);
}
/**
* @return All the available attributes in the execution chain.
*/
@Nonnull
Map attributes();
/**
* Attribute by name.
*
* @param name Attribute's name.
* @param Attribute's type.
* @return Attribute value.
*/
@SuppressWarnings("unchecked")
@Nonnull
default T attr(final String name) {
return (T) attributes().get(name);
}
/**
* Explicit renderer to use or null
.
*
* @return Explicit renderer to use or null
.
*/
@Nonnull
String renderer();
/**
* Indicates if the {@link #pattern()} contains a glob character, like ?
,
* *
or **
.
*
* @return Indicates if the {@link #pattern()} contains a glob charecter, like ?
,
* *
or **
.
*/
boolean glob();
/**
* Recreate a route path and apply the given variables.
*
* @param vars Path variables.
* @return A route pattern.
*/
@Nonnull
String reverse(final Map vars);
/**
* Recreate a route path and apply the given variables.
*
* @param values Path variable values.
* @return A route pattern.
*/
@Nonnull
String reverse(final Object... values);
/**
* Normalize a path by removing double or trailing slashes.
*
* @param path A path to normalize.
* @return A normalized path.
*/
@Nonnull
static String normalize(final String path) {
return RoutePattern.normalize(path);
}
/**
* Remove invalid path mark when present.
*
* @param path Path.
* @return Original path.
*/
@Nonnull
static String unerrpath(final String path) {
if (path.charAt(0) == OUT_OF_PATH) {
return path.substring(1);
}
return path;
}
/**
* Mark a path as invalid.
*
* @param path Path.
* @return Invalid path.
*/
@Nonnull
static String errpath(final String path) {
return OUT_OF_PATH + path;
}
/**
* Source information (where the route was defined).
*
* @return Source information (where the route was defined).
*/
@Nonnull
Route.Source source();
/**
* Print route information like: method, path, source, etc... Useful for debugging.
*
* @param indent Indent level
* @return Output.
*/
@Nonnull
default String print(final int indent) {
StringBuilder buff = new StringBuilder();
String[] header = {"Method", "Path", "Source", "Name", "Pattern", "Consumes", "Produces"};
String[] values = {method(), path(), source().toString(), name(), pattern(),
consumes().toString(), produces().toString()};
BiConsumer, Character> format = (v, s) -> {
buff.append(Strings.padEnd("", indent, ' '))
.append("|").append(s);
for (int i = 0; i < header.length; i++) {
buff
.append(Strings.padEnd(v.apply(i), Math.max(header[i].length(), values[i].length()), s))
.append(s).append("|").append(s);
}
buff.setLength(buff.length() - 1);
};
format.accept(i -> header[i], ' ');
buff.append("\n");
format.accept(i -> "-", '-');
buff.append("\n");
format.accept(i -> values[i], ' ');
return buff.toString();
}
/**
* Print route information like: method, path, source, etc... Useful for debugging.
*
* @return Output.
*/
@Nonnull
default String print() {
return print(0);
}
}