Skip to content
Igor edited this page Sep 1, 2018 · 24 revisions

Starting Server

Server server = new Server(serverConfiguration, requestResolvers, requestFilters);
server.start();

Note that it will run as long as virtual machine is up, so use separate thread if you need to. If you want to make sure that everything is closed properly before terminating vm:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    server.stop();
}));

Configuration

#defaults to empty string
contextPath=
#defaults to 8080
port=
#whether to add cors specific headers in response and handle JavaScript OPTIONS request
addCorsHeaders=true
#All defaults to * and are used only if addCorsHeaders is set to true.
allowedOrigin=*
allowedMethods=*
allowedHeaders=*

Above is needed as Plain Old Java properties file:
ServerConfiguration serverConfiguration = new ServerConfiguration(properties);

Request Resolver

List<RequestResolver> requestResolvers = new ArrayList<>();
RequestResolver helloResolver = new RequestResolver("hello/{id:int}", RequestMethod.GET, urlPatternParser, new HelloHandler());
requestResolvers.add(helloResolver);

First argument is url pattern which, together with request method, will be used to resolve request by UrlPatternParser. In case of success, RequestHandler(in example HelloHandler), will handle request. Bright Server provides TypedUrlPatternParser which implements UrlPatternParser and works as follows. For path variables {key:type} in any part of the url will be bind to (try to) parameter with given key and of described type. Valid types are defined by UrlPatternType and are basically java primitives. From example above "hello/{id:int}" you have guarantee that at the moment of handling request path variable with key id and of type int will be accessible. It works similarly for query parameters:

RequestResolver helloResolver = new RequestResolver("hello?message=string", RequestMethod.GET, urlPatternParser, new HelloHandler());

Above will require message parameter. As already said, proper implementation of UrlPatterParser(which TypedUrlPatternParser is) guarantee that you will have all required by url pattern path variables and parameters at the moment of handling request.

public class HelloHandler implements RequestHandler {

   @Override
   public Response handle(ResolvedRequest request) {
       int id = request.getPathVariable("id", Integer.class);
       String message = request.getParameter("message", String.class);
       message = "Received " + message + " from " + id;
       return new PlainTextResponse(ResponseCode.OK, message);
   }

}

Request Filter

List<RequestFilter> requestFilters = new ArrayList<>();
RequestFilter authorizationFilter = new RequestFilter("*", new AnyRequestMethodRule(), filterUrlPatternParser, 
    new AuthorizationHandler());
RequestFilter authorizationSecondFilter = new RequestFilter("hello/*",
    new ListOfRequestMethodRule(RequestMethod.GET, RequestMethod.POST), filterUrlPatternParser,
    new AuthorizationSecondFreePassHandler());
requestFilters.add(authorizationFilter);
requestFilters.add(authorizationSecondFilter);

Filters work similarly to resolvers, but their purpose is different. First argument is again url pattern, second is RequestMethodRule which there are a few handful implementations provided, third is FilterUrlPatternParser for which we have StarSymbolFilterUrlPatternParser as implementation and the last of is very similiar to RequestHandler, ToFilterRequestHandler. Let's focus first on FilterUrlPatternParser:

public interface FilterUrlPatternParser {
    
    boolean isPrimary(String urlPattern);

    boolean match(String url, String urlPattern);
}

When request come to the Server first it goes to the primary filters for which EVERY url is considered a match. Request method is checked separately. In case of StarSymbolFilterUrlPatternParser it understands url patterns as follows:

  • "*" primary, so match all
  • "a/b/*" will need two first segments to be equal to a and b, third can be anything
  • "a/*/b" similarly, first and third need to be equal, second can be anything
  • "a/" url must starts with a and then there can go evertything, meaning both "a/b/c" nad "a/1" will be matched
  • "a/b/" as above, just needs to start with a and then b

After request is matched it goes through ToFilterRequestHandler:

public interface ToFilterRequestHandler {
    Response handle(Request request);
}

Simple example:

public class AuthorizationHandler implements ToFilterRequestHandler {

    private static final String SECRET_TOKEN = "token";

    @Override
    public Response handle(Request request) {
	if (!request.hasHeader(HeaderKey.AUTHORIZATION)) {
	    return new EmptyResponse(ResponseCode.FORBIDDEN);
	}
	String token = request.getHeader(HeaderKey.AUTHORIZATION);
	boolean valid = token.equals(SECRET_TOKEN);
	if (!valid) {
	    return new EmptyResponse(ResponseCode.FORBIDDEN);
	}
	System.out.println("Secret header of " + token + " is valid");
	return new EmptyResponse(ResponseCode.OK);
    }

}

If you return ok response, which is response with the code in 200 - 299 range, request will go to next filter or to resolver. In other case, request is interpreted as wrong one and its response is returned to client.

Customization

While reading you may recognized that most of the abstractions use by Server are interfaces, so if you need, you can customize a lot of things. For example, you can create Server by this constructor:

public Server(ServerConfiguration serverConfiguration, RequestResponseParser requestReponseParser,
    List<RequestResolver> requestsResolvers, List<RequestFilter> requestFilters) {
         this(serverConfiguration, Executors.newCachedThreadPool(), requestReponseParser, requestsResolvers,
         requestFilters);
}

As you can see there is RequestResponseParser, which is again an interface:

public interface RequestResponseParser {

    Request read(InputStream inputStream) throws IOException;

    byte[] write(Response response);
}

If you are not happy with defualt HttpOneParser you can implement your own parsing logic. Note though, that if you use your own parser cors headers if set in ServerConfiguration will not be added, because response is write only by this interface. If you want to use default parser but add some custom headers to every response:

public Server(ServerConfiguration serverConfiguration, List<RequestResolver> requestsResolvers,
     List<RequestFilter> requestFilters, List<Header> additionalResponseHeaders)

Lastly, you can implement you own UrlPattern nad its parsing logic. In case of resolving request, just implement:

public interface UrlPatternParser {

    boolean match(String url, String urlPattern);
    
    Pairs readPathVariables(String url, String urlPattern);
    
    Pairs readParameters(String url, String urlPattern);
    
    boolean hasParameters(String urlPattern);
    
    boolean hasPathVariables(String urlPattern);
}

Or/and customize you FilterUrlPattern:

public interface FilterUrlPatternParser {

    boolean isPrimary(String urlPattern);

    boolean match(String url, String urlPattern);
}

In 99% cases all you need is provided, default implementations, but as you can see it is easy to customize almost all of the logic.

Optimization

Server is listening on one thread, but every request is handled on separate one provided by Java's Executor interface:

Socket socket = serverSocket.accept();
executor.execute(getRequestHandler(socket));

By default Bright Server use Executors.newCachedThreadPool(), which probably will be enough for most cases, but if you need/want to change it, just create Server as follows:

public Server(ServerConfiguration serverConfiguration, Executor executor, List<RequestResolver> requestsResolvers, 
   List<RequestFilter> requestFilters)

Examples

Check example package. There is ServerApplication which have main method. You can run is as simple Java Application which will start the Server.

Clone this wiki locally