-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Server server = new Server(8080, 5000, connector);
server.start();
Note that it will block current thread, ideally for as long as virtual machine is running, so use separate thread if you need to.
Connection connection = new RequestResponseConnection(new HttpOneProtocol(), application);
It is a simple interface that handles every socket given it by the Server.
public interface Connection {
void connect(Socket socket);
}
RequestResponseConnection is suited for most cases implementation that has a list of Applications, to which he gives Requests and receives Responses. He has access to Request and Response objects thanks to RequestResponseProtocol.
public interface RequestResponseProtocol {
Request request(InputStream inputStream) throws Exception;
void write(OutputStream outputStream, Response response) throws Exception;
boolean shouldClose(Request request);
}
Application is even simpler.
public interface Application {
Optional<Response> response(Request request);
}
It should return response if it is interested in responding to given request or nothing otherwise. In RequestResponseConnection case, he will return 404 if he hasn't received any response. For most cases, you probably will have just one Application. BrightServer provides HttpApplication which implements Application and will be good enough for most use cases. Having explained details, let's see how RequestResponseConnection is using them together.
@Override
public void connect(Socket socket) {
try (InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream()) {
Request request = this.protocol.request(is);
Response response = response(request);
this.protocol.write(os, response);
if (this.protocol.shouldClose(request)) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
close(socket);
}
};
private Response response(Request request) {
for (Application a : this.applications) {
Optional<Response> r = a.response(request);
if (r.isPresent()) {
return r.get();
}
}
return new NotFoundResponse();
}
That's the high level overview, now let's analyze the details.
List<ConditionalRespondent> respondents = new ArrayList<>();
ConditionalRespondent helloRespondent = new PotentialRespondent("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 PotentialRespondent("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.
@Override
public Response response(MatchedRequest request) {
Response response;
try {
int id = request.pathVariables().intValue("id");
String message = "Hello number " + id;
response = new OkResponse(message);
} catch (Exception e) {
response = new BadRequestResponse(e.getMessage());
}
return response;
}
List<ConditionalRequestFilter> filters = new ArrayList<>();
ConditionalRequestFilter authorizationFilter = new PotentialFilter("*", new AnyRequestMethodRule(),
new AuthorizationFilter());
ConditionalRequestFilter authorizationSecondFilter = new PotentialFilter(new ListOfRequestMethodRule(get, post),
new AuthorizationSecondFreePassFilter(), "complex/", "hello/*");
filters.add(authorizationFilter);
filters.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 AsteriskFilterUrlPattern as implementation and the last of is very similiar to Respondent, RequestFilter. In the second example(authorizationSecondFilter) another constructor is used. It allows binding one RequestMethodRule and many ToFilterUrlPatterns with one filter. It is also possible to bind one filter to RequestMethodRule and ToFilterUrlPatter pair by using:
public PotentialFilter(Filter filter, FilterRule... filterRules) {
this(Arrays.asList(filterRules), filter);
}
FilterRule is simple interface with handy implementation:
public final class ToFilterRequestRule implements FilterRule {
private final RequestMethodRule methodRule;
private final FilterUrlPattern urlPattern;
public ToFilterRequestRule(RequestMethodRule methodRule, FilterUrlPattern urlPattern) {
this.methodRule = methodRule;
this.urlPattern = urlPattern;
}
public ToFilterRequestRule(RequestMethodRule methodRule, String urlPattern) {
this(methodRule, new AsteriskFilterUrlPattern(urlPattern));
}
@Override
public boolean isCompliant(Request value) {
return this.methodRule.isCompliant(value.method()) && this.urlPattern.isMatched(value.url());
}
@Override
public boolean isPrimary() {
return this.urlPattern.isPrimary();
}
}
Now, let's focus first on FilterUrlPattern:
public interface FilterUrlPattern {
boolean isPrimary();
boolean isMatched(String url);
}
When request come to the Server at first it goes to the primary filters for which EVERY url is considered a match. Request method is checked separately. In case of AsteriskFilterUrlPattern 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 everything, meaning both "a/b/c" and "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 Filter:
public interface Filter {
Response response(Request request);
}
Simple example:
public final class AuthorizationFilter implements Filter {
private static final String SECRET_TOKEN = "token";
private static final String AUTHORIZATION_HEADER = "Authorization";
@Override
public Response response(Request request) {
Response response;
if (request.hasHeader(AUTHORIZATION_HEADER)) {
String token = request.header(AUTHORIZATION_HEADER);
boolean valid = token.equals(SECRET_TOKEN);
response = valid ? new OkResponse() : new ForbiddenResponse();
} else {
response = new ForbiddenResponse();
}
return response;
}
}
If you return ok response, which is a response with the code in 200 - 299 range, request will go to the next filter or to a respondent. In other case, request is interpreted as wrong one and its response is returned to a client.
While reading you may recognized that most of the used abstractions are interfaces, so if you need, you can customize almost everything. For example, if you do not like provided HttpOneProtocol you can create your own implementation of RequestResponseProtocol and give it to RequestResponseConnection or even implement your own Connection and so on. You can provide UrlPattern implementation and have control over determining if request will be handled by a particular respondent.
public interface UrlPattern {
boolean isMatched(String url);
TypedMap pathVariables(String url);
TypedMap parameters(String url);
boolean hasParameters();
boolean hasPathVariables();
}
Or/and customize FilterUrlPattern.
public interface FilterUrlPattern {
boolean isPrimary();
boolean match(String url);
}
Or/and customize FileUrlPattern.
public interface FileUrlPattern {
boolean isMatched(String url);
String filePath(String url);
}
In 90% cases all you need is provided, default implementations, but as you can see it is very easy to change almost all of the logic.
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);
this.executor.execute(() -> this.connection.connect(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, Connection connection)