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

Starting Server

Server server = new Server(serverConfiguration, respondents, filters);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    server.stop();
}));
server.start();

Note that it will block current thread for as long as virtual machine is running, so use separate thread if you need to.

Configuration

#defaults to empty string
contextPath=
#defaults to 8080
port=
#defaults to 5000(milliseconds)
timeout=
#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 BrightServerConfiguration(properties);

It is an interface, so if you do not like above you can easily create your own configuration:

public interface ServerConfiguration {

   String contextPath();

   int port();

   int timeout();

   Cors cors();
}

ConditionalRespondent

List<ConditionalRespondent> respondents = new ArrayList<>();
ConditionalRespondent helloRespondent = new HttpRespondent("hello/{id:int}", get, new HelloRespondent());
respondents.add(helloRespondent);

First argument is url pattern which, together with request method, will be used to determine whether or not particualar instance of respondent should respond to request. By default HttpRespondent use TypedUrlPattern to determine whether or not he should respond to request and read its parameters and variables. It 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 responding to request path variable with key id and of type int will be accessible. It works similarly for query parameters:

ConditionalRespondent complexRespondent = new HttpRespondent("complex/search?message=string&scale=float", post, new ComplexUrlRespondent());

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

public class HelloRespondent implements Respondent {

   @Override
   public Response respond(MatchedRequest request) throws Exception {
       int id = request.pathVariable("id", Integer.class);
       String message = "Hello number " + id;
       return new OkResponse(new TextPlainContentTypeHeader(), message);
   }
}

ConditionalRequestFilter

List<ConditionalRequestFilter> requestFilters = new ArrayList<>();
ConditionalRequestFilter authorizationFilter = new HttpRequestFilter("*", new AnyRequestMethodRule(),
	new AuthorizationFilter());
ConditionalRequestFilter authorizationSecondFilter = new HttpRequestFilter("hello/*",
	new ListOfRequestMethodRule(get, post), new AuthorizationSecondFreePassFilter());
requestFilters.add(authorizationFilter);
requestFilters.add(authorizationSecondFilter);

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

public interface ToFilterUrlPattern {

    boolean isPrimary();

    boolean match(String url);
}

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 StarSymbolFilterUrlPattern 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 ConditionalRequestFilter:

public interface RequestFilter {
    Response filter(Request request) throws Exception;
}

Simple example:

public class AuthorizationFilter implements RequestFilter {

    private static final String SECRET_TOKEN = "token";
    private static final String AUTHORIZATION_HEADER = "Authorization";

    @Override
    public Response filter(Request request) throws Exception {
	if (!request.hasHeader(AUTHORIZATION_HEADER)) {
	    return new ForbiddenResponse();
	}
	String token = request.header(AUTHORIZATION_HEADER);
	boolean valid = token.equals(SECRET_TOKEN);
	if (!valid) {
	    return new ForbiddenResponse();
	}
	return new OkResponse();
    }

}

If you return ok response, which is response with the code in 200 - 299 range, request will go to next filter or to respondent. In other case, request is interpreted as wrong one and its response is returned to a 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, RequestResponseProtocol requestReponseProtocol,
	    List<ConditionalRespondent> respondents, List<ConditionalRequestFilter> requestFilters) throws IOException {
	this(serverConfiguration, Executors.newCachedThreadPool(), requestReponseParser, respondents, requestFilters);
}

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

public interface RequestResponseProtocol {

    Request read(InputStream inputStream) throws Exception;

    void write(OutputStream outputStream, Response response) throws Exception;
}

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

public Server(ServerConfiguration serverConfiguration, List<ConditionalRespondent> respondents,
    List<ConditionalRequestFilter> requestFilters, List<Header> additionalResponeHeaders) throws IOException {
      this(serverConfiguration, Executors.newCachedThreadPool(), new 
        HttpOneProtocol(serverConfiguration.cors().toAddHeaders()), respondents, requestFilters);
}

Lastly, you can implement you own UrlPattern. In case of determing if request will be handled by respondents, just implement:

public interface UrlPattern {

    boolean match(String url);

    KeysValues readPathVariables(String url);

    KeysValues readParameters(String url);

    boolean hasParameters();

    boolean hasPathVariables();
}

Or/and customize ToFilterUrlPattern:

public interface ToFilterUrlPattern {

    boolean isPrimary();

    boolean match(String url);
}

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(handleConnection(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<ConditionalRespondent> respondents,
	  List<ConditionalRequestFilter> requestFilters) throws IOException {
      this(serverConfiguration, executor, new HttpOneProtocol(serverConfiguration.cors().toAddHeaders()), respondents,
	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