Controller
Another approach to handling a request and producing a response is using Controllers. After routing has determined what controller to use, an action method will be invoked. In Pippo, controllers are instances of Controller.
How to use
Defining a new controller is simple:
@Path("/contacts")
@Logging
public class ContactsController extends Controller {
private ContactService contactService;
public ContactsController() {
contactService = new InMemoryContactService();
}
@GET
@Named("index")
// @Produces(Produces.HTML)
@Metered
@Logging
public void index() {
// inject "user" attribute in session
getRouteContext().setSession("user", "decebal");
// send a template with name "contacts" as response
getResponse()
.bind("contacts", contactService.getContacts())
.render("contacts");
}
@GET("/uriFor/{id: [0-9]+}")
@Named("uriFor")
@Produces(Produces.TEXT)
@Timed
public String uriFor(@Param int id, @Header String host, @Session String user) {
System.out.println("id = " + id);
System.out.println("host = " + host);
System.out.println("user = " + user);
Map<String, Object> parameters = new HashMap<>();
parameters.put("id", id);
String uri = getApplication().getRouter().uriFor("api.get", parameters);
return "id = " + id + "; uri = " + uri;
}
@GET("/api")
@Named("api.getAll")
@Produces(Produces.JSON)
@NoCache
public List<Contact> getAll() {
return contactService.getContacts();
}
@GET("/api/{id: [0-9]+}")
@Named("api.get")
@Produces(Produces.JSON)
public Contact get(@Param int id) {
return contactService.getContact(id);
}
}
@Path("/files")
public class FilesController extends Controller {
@GET
public void index() {
// send a template with name "files" as response
getRouteContext().render("files");
}
@GET("/download")
public File download() {
// send a file as response
return new File("pom.xml");
}
@POST("/upload")
@Produces(Produces.TEXT)
public String upload(FileItem file) {
// send a text (the info about uploaded file) as response
// return file.toString();
return new StringBuilder()
.append(file.getName()).append("\n")
.append(file.getSubmittedFileName()).append("\n")
.append(file.getSize()).append("\n")
.append(file.getContentType())
.toString();
}
}
Methods attached to the controller (for example index
from above snippet) are known as action methods. When Pippo receives a request, it will create a new instance of the controller and call the appropriate action method.
You can register a controller’s action in application with a simple line:
public class MyApplication extends ControllerApplication {
@Override
protected void onInit() {
// add controller(s)
addControllers(ContactsController.class); // one instance for EACH request
// or
addControllers(new ContactsController()); // one instance for ALL requests
addControllers(FilesController.class);
}
}
public class ControllerDemo {
public static void main(String[] args) {
Pippo pippo = new Pippo(new MyApplication());
pippo.start();
}
}
DON’T forget to add pippo-controller
module as dependency in your project.
If you use Maven, your pom.xml
must contains above lines:
<dependency>
<groupId>ro.pippo</groupId>
<artifactId>pippo-controller</artifactId>
<version>${pippo.version}</version>
</dependency>
Extractors
With few words a method parameter extractor create a method parameter value from the RouteContext
and the MethodParameter
(an useful wrapper over Parameter class from Java).
For example, see the controller route defined with method:
void uriFor(@Param int id, @Param String action, @Header String host, @Session String user);
When a request comes for the above route, Pippo uses extractors to extract values for the method’s parameters: id
, action
, host
and user
.
Pippo comes with some builtin extractors that extract values from multiple locations:
Param
extracts value from the request parameter (similar withrequest.getParamater()
)Header
extracts value from the request headerSession
extracts value from a session’s attributeBody
extract value from the request body cia a Content Type Engine (e.g. JSON, XML)
They are also other builtin extractors that are not presented in this post. The beauty of Pippo here is that you can define any extractor you want/need with no effort.
All you must to do is to implement MethodParameterExtractor
interface:
public interface MethodParameterExtractor {
/**
* Returns true if this extractor is applicable to the given {@link MethodParameter}.
*/
boolean isApplicable(MethodParameter parameter);
/**
* Extract a value from a {@link MethodParameter} for a specified {@link RouteContext}.
*/
Object extract(MethodParameter parameter, RouteContext routeContext);
}
Example, BeanExtractor
that create a bean from the request parameters:
@MetaInfServices
public class BeanExtractor implements MethodParameterExtractor {
@Override
public boolean isApplicable(MethodParameter parameter) {
return parameter.isAnnotationPresent(Bean.class);
}
@Override
public Object extract(MethodParameter parameter, RouteContext routeContext) {
Class<?> parameterType = parameter.getParameterType();
return routeContext.createEntityFromParameters(parameterType);
}
}
After you create the new extractor you should add it to Pippo:
- automatically (annotate your extractor with
@MetaInfServices
) - manually (via
ControllerApplication.addExtractors(extractor)
method)
You are not forced to create an Extractor using only annotation (it’s not annotation driven).
Below, I present you a new extractor, FileItemExtractor
that does’t use annotation (it looks after parameter type).
@MetaInfServices
public class FileItemExtractor implements MethodParameterExtractor {
@Override
public boolean isApplicable(MethodParameter parameter) {
return FileItem.class == parameter.getParameterType();
}
@Override
public Object extract(MethodParameter parameter, RouteContext routeContext) {
String name = parameter.getParameterName();
return routeContext.getRequest().getFile(name);
}
}
The signature for a possible controller’s route that uses a FileItemExtractor can might be:
void upload(FileItem file);
Parameter name
The Controller module may depend on the -parameters
flag of the Java 8 javac compiler. This flag embeds the names of method parameters in the generated .class files.
By default Java 8 does not compile with this flag set so you must specify javac compiler arguments for Maven and your IDE.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<parameters/>
</compilerArguments>
</configuration>
</plugin>
</plugins>
</build>
So, if you you compile with -parameters
flag you can define the controller method like:
void invoice(@Param orderId) { ... }
instead of:
void invoice(@Param("orderId") orderId) { ... }
If you use the first variant without -parameters
flag on compile, you will receive an exception like:
ro.pippo.core.PippoRuntimeException: Method 'acme.controller.MyController::invoice' parameter 0 does not specify a name!
at ro.pippo.controller.extractor.ParamExtractor.getParameterName(ParamExtractor.java:69)
at ro.pippo.controller.extractor.ParamExtractor.extract(ParamExtractor.java:44)
at ro.pippo.controller.ControllerRouteHandler.prepareMethodParameters(ControllerRouteHandler.java:388)
at ro.pippo.controller.ControllerRouteHandler.handle(ControllerRouteHandler.java:120)
at ro.pippo.core.route.DefaultRouteContext.handleRoute(DefaultRouteContext.java:394)
at ro.pippo.core.route.DefaultRouteContext.next(DefaultRouteContext.java:276)
at ro.pippo.core.route.RouteDispatcher.onRouteDispatch(RouteDispatcher.java:153)
at ro.pippo.core.route.RouteDispatcher.dispatch(RouteDispatcher.java:101)
at ro.pippo.core.PippoFilter.processRequest(PippoFilter.java:179)
Implementation details
Advanced features (Extractor, Interceptor, …) and technical aspects are presented in details here.
You can see a demo here