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

Basics

Server server = new Server(8080, 5000, connector);
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.

Connector connector = new RequestResponseConnector(new HttpOneProtocol(), application);

Connector is a simple interface that handle socket every connection gived by the Server:

public interface Connector {
    Runnable plug(Socket socket);
}

RequestResponseConnector is suited for most cases implementations that has list of Applications, to which he gives Requests and is receiving Responses, if any of the Applications is interested in it. He has access to Request/Response objects thanks to RequestResponseProtocol:

public interface RequestResponseProtocol {

    Request read(InputStream inputStream) throws Exception;

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

    boolean closeConnection(Request request);
}

Application is even simpler:

public interface Application {
    Optional<Response> respond(Request request);
}

It should return response if it is interested in responding to given request or nothing otherwise. For most cases, you probably will have just one instance of it. BrightServer provides WebApplication which implements Application and will be good enough for most use cases, but you can provide your own implementation. Having explained details, let's see how RequestResponseProtocol is using them:

@Override
public Runnable plug(Socket socket) {
    return () -> {
        try (InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
	    Request request = protocol.read(inputStream);
            Response response = respond(request);
	    protocol.write(outputStream, response);
	    if (protocol.closeConnection(request)) {
                socket.close();
	    }
	 } catch (Exception exception) {
	     exception.printStackTrace();
	     closeConnection(socket);
        }
    };
}

That's the high level overview, now let's analyze the details.

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 particular 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 used abstractions use are interfaces, so if you need, you can customize a lot of things. For example, if you do not like provided HttpOneProtocol you can give your own implementation of RequestResponseProtocol to RequestResponseConnector or even implement your own Connector and so on. 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();
socket.setSoTimeout(timeout);
executor.execute(connector.plug(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 using constructor:

public Server(int port, int timeout, Executor executor, Connector connector)

Examples

Examples package.

Clone this wiki locally