Skip to content

Action Controller micro framework for easy REST backends

License

Notifications You must be signed in to change notification settings

norrs/action-controller

 
 

Repository files navigation

Apache 2.0 License Maven Central Javadoc Build Status Coverage Status Vulnerability scan

Action Servlet micro REST framework

Action Servlet Framework lets you create simple REST Controllers with minimum of magic.

Example API:

public class MyApiServlet extends ApiServlet {

    public MyApiServlet() {
        super(new MyApiController());
    }
}


public class MyApiController {

    @GET("/v1/api/objects")
    @JsonBody
    public List<SomePojo> listObjects(
        @RequestParam("query") Optional<String> query,
        @RequestParam("maxHits") Optional<Integer> maxHits
    ) {
        // ... this is up to you
    }

    @GET("/v1/api/objects/:id")
    @JsonBody
    public SomePojo getObject(@PathParam("id") UUID id) {
        // ... this is up to you
    }

    @POST("/v1/api/objects/")
    @SendRedirect
    public String postData(
        @JsonBody SomePojo myPojo,
        @SessionParameter("user") Optional<User> user
    ) {
        // ... do your thing
        return "/home/";
    }
    
    @GET("/oauth2callback")
    @SendRedirect
    public String establishUserSession(
            @RequestParam("code") String authenticationCode,
            @SessionParameter(value = "userProfile", invalidate=true) Consumer<UserProfile> setUserProfile
    ) {
        // ...
        setUserProfile.apply(newlyLoggedInUser);
        return "/profile";
    }

}

The inner workings

The magic that makes Action Controller work is the annotations like @PathParam and @JsonBody. The set of annotations is actually extensible. Here's how @RequestParam is defined:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@HttpParameterMapping(RequestParam.RequestParameterMappingFactory.class)
public @interface RequestParam {

    String value();
}

@Retention tells javac to keep the information about the annotation available for reflection (by default, annotations are only used by the compiler). @Target tells javac to only allow this annotation on method parameters (as opposed to, for example class declarations).

@HttpParameterMapping tells Action Controller to use this annotation to resolve the value of a action method parameter. The RequestParam.RequestParameterMappingFactory describes what Action Controller should do with the annotation. Here's how it's defined:

public class RequestParameterMappingFactory extends HttpRequestParameterMappingFactory<RequestParam> {
        @Override
        public HttpRequestParameterMapping create(RequestParam annotation, Parameter parameter) {
            String name = annotation.value();
            return (exchange) -> exchange.getParameter(name, parameter);
        }
}

Action Servlet instantiates the mapping factory with a default constructor and invokes create, which lets the factory set up the mapping with properties from the annotation. The mapper itself takes an ApiHttpExchange (which encapsulates the HTTP request and the response) and returns the value to use for the method parameter on the action controller.

That's really all there it to it! :-)

Running with Jetty

import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContext;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;

public class MyServer {
    public class MyListener implements ServletContextListener {

        @Override
        public void contextInitialized(ServletContextEvent sce) {
            ServletContext context = sce.getServletContext();
            context.addServlet("api", new ApiServlet(new MyApiController())).addMapping("/api/*");
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent sce) {    
        }
    }


    public static void main(String[] args) {
        Server server = new Server(8080);
        ServletContextHandler handler = new ServletContextHandler();
        handler.setContextPath("/demo");
        handler.addEventListener(new MyListener());
        server.setHandler(handler);
        server.start();
    }
}

Running with web.xml

  1. Implemement a ServletContextListener which creates ApiServlet
  2. Add the ServletContextListener to your web.xml file
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContext;

public class MyListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        context.addServlet("api", new ApiServlet(new MyApiController())).addMapping("/api/*");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {    
    }
}
<web-app>
  <listener>
    <listener-class>
       MyListener
    </listener-class>
  </listener>
</web-app>

Running with JDK HttpServer

import com.sun.net.httpserver.HttpServer;
import org.actioncontroller.httpserver.ApiHandler;

public class MyServer {
    public static void main(String[] args){
          HttpServer httpServer = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
          httpServer.createContext("/demo/api", new ApiHandler(new MyApiController()));
          httpServer.start();
    }
}

Api client

interface MyApi {
    
    @GET("/v1/api/objects")
    @JsonBody
    List<SomePojo> listObjects(
        @RequestParam("query") Optional<String> query,
        @RequestParam("maxHits") Optional<Integer> maxHits
    );

    @GET("/v1/api/objects/:id")
    @JsonBody
    SomePojo getObject(@PathParam("id") long id);
}

public class ApiClientDemo {
    public static void main(String[] args){
        HttpURLConnectionApiClient client = new HttpURLConnectionApiClient("http://localhost:8080/api");
        MyApi myApi = ApiClientProxy.create(MyApi.class, httpClient);
        
        // Will perform HTTP GET of http://localhost:8080/api/v1/api/objects/5001
        SomePojo object = myApi.getObject(50001);
    }
}

ConfigObserver

ConfigObserver is a revolutionary way to think of application configuration. ConfigObserver monitors the configuration values and calls back to you application when a configuration value you care about is changed. This way, your application can easily hot-reload configuration values.

Example:

public class MyApplication {
    
    private String myConfigValue;
    private DataSource dataSource;
    private ServerSocket serverSocket;

    public MyApplication(ConfigObserver config) {
        config.onStringValue("myConfigValue", null, v -> this.myConfigValue = v);
        config.onPrefixedValue("dataSource", DataSourceConfig::create, dataSource -> this.dataSouce = dataSource);
        config.onInetSocketAddress("serverAddress", 10080,
                address -> {
                    if (serverSocket != null) serverSocket.close();
                    serverSocket = new ServerSocket(address);
                    startServerSocket(serverSocket);
                });
    }

    public static void main(String[] args){
      ConfigObserver config = new ConfigObserver(new File("."), "myApp");
      new MyApplication(config);
    }
}

TODO

  • Log payloads
  • Split HttpClientParameterMapping from HttpParameterMapping?

About

Action Controller micro framework for easy REST backends

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Java 99.8%
  • HTML 0.2%