Takes is a true object-oriented and immutable Java6 web development framework. Its key benefits, comparing to all others, include these four fundamental principles:
- not a single
null
(why NULL is bad?) - not a single
public
static
method (why they are bad?) - not a single mutable class (why they are bad?)
- not a single
instanceof
keyword, type casting, or reflection
Of course, there are no configuration files. Besides that, these are more traditional features, out of the box:
- hit-refresh debugging
- XML+XSLT
- JSON
- RESTful
- Templates, incl. Apache Velocity
This is what is not supported and won't be supported:
Create this App.java
file:
import org.takes.http.Exit;
import org.takes.http.FtBasic;
import org.takes.facets.fork.TsFork;
public final class App {
public static void main(final String... args) throws Exception {
new FtBasic(
new TsFork(new FkRegex("/", "hello, world!")), 8080
).start(Exit.NEVER);
}
}
Then, download takes.jar
and compile your Java code:
$ javac -cp takes.jar App.java
Now, run it like this:
$ java -Dfile.encoding=UTF-8 -cp takes.jar:. App
Should work :)
This code starts a new HTTP server on port 8080 and renders a plain-text page on all requests at the root URI.
Important: Pay attention that UTF-8 encoding is set on the command line.
The entire framework relies on your default Java encoding, which is not
necessarily UTF-8 by default. To be sure, always set it on the command line
with file.encoding
Java argument.
If you're using Maven, this is how your pom.xml
should look like:
<project>
<dependencies>
<dependency>
<groupId>org.takes</groupId>
<artifactId>takes</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>
<id>hit-refresh</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.3</version>
<executions>
<execution>
<id>start-server</id>
<phase>pre-integration-test</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>foo.App</mainClass> <!-- your main class -->
<cleanupDaemonThreads>false</cleanupDaemonThreads>
<arguments>
<argument>--port=${port}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
With this configutation you can run it from command line:
$ mvn clean integration-test -Phit-refresh -Dport=8080
Maven will start the server and you can see it at http://localhost:8080
.
This is how you can unit test the app, using JUnit 4.x and Hamcrest:
public final class AppTest {
@Test
public void returnsHttpResponse() throws Exception {
MatcherAssert.assertThat(
new RsPrint(
new App().route(new RqFake("GET", "/")).act()
).printBody(),
Matchers.equalsTo("hello, world!")
);
}
}
Here is how you can test the entire server via HTTP, using JUnit and jcabi-http for making HTTP requests:
public final class AppITCase {
@Test
public void returnsTextPageOnHttpRequest() throws Exception {
new FtRemote(new App()).exec(
new FtRemote.Script() {
@Override
public void exec(final URI home) throws IOException {
new JdkRequest(home)
.fetch()
.as(RestResponse.class)
.assertStatus(HttpURLConnection.HTTP_OK)
.assertBody(Matchers.equalTo("hello, world!"));
}
}
);
}
}
More complex integration testing examples you can find in one of the open source projects that are using Takes, for example: rultor.com.
Let's make it a bit more sophisticated:
public final class App {
public static void main(final String... args) {
new FtBasic(
new TsFork(
new FkRegex("/robots\\.txt", ""),
new FkRegex("/", new TkIndex())
),
8080
).start(Exit.NEVER);
}
}
The FtBasic
is accepting new incoming sockets on port 8080,
parses them according to HTTP 1.1 specification and creates instances
of class Request
. Then, it gives requests to the instance of TsFork
(ts
stands for "takes") and expects it to return an instance of Take
back.
As you probably understood already, the first regular expression that matches
returns a take. TkIndex
is our custom class (tk
stands for "take"),
let's see how it looks:
public final class TkIndex implements Take {
@Override
public Response act() {
return new RsHtml("<html>Hello, world!</html>");
}
}
It is immutable and must implement a single method act()
, which is returning
an instance of Response
. So far so good, but this class doesn't have an access
to an HTTP request. Here is how we solve this:
new TsFork(
new FkRegex(
"/file/(?<path>[^/]+)",
new Target<RqRegex>() {
@Override
public Take route(final RqRegex request) {
final File file = new File(
request.matcher().group("path")
);
return new TkHTML(
FileUtils.readFileToString(file, Charsets.UTF_8)
);
}
}
)
)
We're using Target<RqRequest>
instead of Takes
, in order to deal with
RqRegex
instead of a more generic Request
. RqRegex
gives an instance
of Matcher
used by TsFork
for pattern matching.
Here is a more complex and verbose example:
public final class App {
public static void main(final String... args) {
new FtBasic(
new TsFork(
new FkRegex("/robots.txt", ""),
new FkRegex("/", new TkIndex()),
new FkRegex(
"/xsl/.*",
new TsWithType(new TsClasspath(), "text/xsl")
),
new FkRegex(
"/account",
new Takes() {
@Override
public Take route(final Request request) {
return new TkAccount(users, request);
}
}
),
new FkRegex(
"/balance/(?<user>[a-z]+)",
new Target<RqRegex>() {
@Override
public Take route(final RqRegex request) {
return new TkBalance(request.matcher().group("user"));
}
}
)
)
).start(Exit.NEVER);
}
}
Now let's see how we can render something more complex than an plain text.
First, XML+XSLT is a recommended mechanism of HTML rendering. Even though it may
too complex, give it a try, you won't regret. Here is how we render a simple XML
page that is transformed to HTML5 on-fly (more about RsXembly
read below):
public final class TkAccount implements Take {
private final User user;
public TkAccount(final Users users, final Request request) {
this.user = users.find(new RqCookies(request).get("user"));
}
@Override
public Response act() {
return new RsLogin(
new RsXSLT(
new RsXembly(
new XeStylesheet("/xsl/account.xsl"),
new XeAppend("page", this.user)
)
),
this.user
);
}
}
This is how that User
class may look like:
public final class User implements XeSource {
private final String name;
private final int balance;
@Override
public Iterable<Directive> toXembly() {
return new Directives().add("user")
.add("name").set(this.name).up()
.add("balance").set(Integer.toString(this.balance));
}
}
Here is how RsLogin
may look like:
public final class RsLogin extends RsWrap {
public RsLogin(final Response response, final User user) {
super(
new RsWithCookie(
response, "user", user.toString()
)
);
}
}
Let's say, you want to use Velocity:
public final class TkHelloWorld implements Take {
@Override
public Response act() {
return new RsVelocity("hi, ${user.name}! You've got ${user.balance}")
.with("user", new User());
}
}
You will need this extra dependency in classpath:
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<scope>runtime</scope>
</dependency>
Very often you need to serve static resources to your web users, like CSS stylesheets, images, JavaScript files, etc. There are a few supplementary classes for that:
new TsFork(
new FkRegex("/css/.+", new TsWithType(new TsClasspath(), "text/css")),
new FkRegex("/data/.+", new TsFiles(new File("/usr/local/data"))
)
Class TsClasspath
takes static part of the request URI and finds a resource with this name in classpath.
TsFiles
just looks by file name in the directory configured.
TsWithType
sets content type of all responses coming out of the decorated takes.
It is a very convenient feature. Once you start the app you want to be able to modify its static resources (CSS, JS, XSL, etc), refresh the page in a browser and immediately see the result. You don't want to re-compile the entire project and restart it. Here is what you need to do to your sources in order to enable that feature:
new TsFork(
new FkRegex(
"/css/.+",
new TsWithType(
new TsFork(
new FkHitRefresh(
"./src/main/resources/foo/scss/**", // what sources to watch
"mvn sass:compile", // what to run when sources are modified
new TsFiles("./target/css")
)
new FkFixed(new TsClasspath())
),
"text/css"
)
)
)
This FkHitRefresh
fork is a decorator of takes. Once it sees
X-Takes-Refresh
header in the request, it realizes that the server is running in
"hit-refresh" mode and passes the request to the encapsulated takes. Before it
passes the request it tries to understand whether any of the resources
are older than compiled files. If they are older, it tries
to run compilation tool to build them again.
Here is an example:
new TsFork(
new FkRegex(
"/user",
new TsFork(
new FkMethods("GET", new TkGetUser()),
new FkMethods("POST,PUT", new TkPostUser()),
new FkMethods("DELETE", new TkDeleteUser())
)
)
)
Here is how you can parse an instance of Request
:
Href href = new RqHref(request).href();
URI uri = href.uri();
List<String> values = href.param("key");
For a more complex parsing try to use Apache Http Client or something similar.
Here is an example:
public final class TkSavePhoto implements Take {
private final RqForm request;
public TkSavePhoto(final Request req) {
this.request = new RqForm(req);
}
@Override
public Response act() {
final String name = this.request.param("name");
return new RsWithStatus(HttpURLConnection.HTTP_NO_CONTENT);
}
}
By default, TsFork
lets all exceptions bubble up. If one of your takes
crashes, a user will see a default error page. Here is how you can configure
this behavior:
public final class App {
public static void main(final String... args) {
new FtBasic(
new TsFallback(
new TsFork(
new FkRegex("/robots\\.txt", ""),
new FkRegex("/", new TkIndex())
)
new TkHTML("oops, something went wrong!")
),
8080
).start(Exit.NEVER);
}
}
TsFallback
decorates an instance of Takes and catches all exceptions any of
its takes may throw. Once it's thrown, an instance of TkHTML will be returned.
Sometimes it's very useful to return a redirect response (30x
status code),
either by a normal return
or by throwing an exception. This example
illustrates both methods:
public final class TkPostMessage implements Take {
private final Request request;
public TkPostMessage(final Request req) {
this.request = req;
}
@Override
public Response act() {
final String body = new RqPring(this.request).printBody();
if (body.isEmpty()) {
throw new RsFlash(
new RsForward(),
"message can't be empty"
);
}
// save the message to the database
return new RsFlash(
new RsForward("/"),
"thanks, the message was posted"
);
}
}
Then, you should decorate the entire TsFork
with this TsForward
and TsFlash
:
public final class App {
public static void main(final String... args) {
new FtBasic(
new TsFlash(
new TsForward(
new TsFork(new FkRegex("/", new TkPostMessage())
)
),
8080
).start(Exit.NEVER);
}
}
Here is how we can deal with JSON:
public final class TkBalance extends TkFixed {
public TkBalance(final RqRegex request) {
super(
new RsJSON(
new User(request.matcher().group("user")))
)
);
}
}
This is the method to add to User
:
public final class User implements XeSource, RsJSON.Source {
@Override
public JsonObject toJSON() {
return Json.createObjectBuilder()
.add("balance", this.balance)
.build();
}
}
Here is how you generate an XML page using Xembly:
Response response = new RsXembly(
new XeAppend("page"),
new XeDirectives("XPATH '/page'", this.user)
)
This is a complete example, with all possible options:
Response response = new RsXembly(
new XeStylesheet("/xsl/account.xsl"), // add processing instruction
new XeAppend(
"page", // create a DOM document with "page" root element
new XeMillis(false), // add "millis" attribute to the root, with current time
this.user, // add this.user to the root element
new XeSource() {
@Override
public Iterable<Directive> toXembly() {
return new Directives().add("status").set("alive");
}
},
new XeMillis(true), // replace "millis" attribute with take building time
),
)
This is the output that will be produced:
<?xml version='1.0'?>
<?xsl-stylesheet href='/xsl/account.xsl'?>
<page>
<millis>5648</millis>
<user>
<name>Jeff Lebowski</name>
<balance>123</balance>
</user>
<status>alive</status>
</page>
To avoid duplication of all this scaffolding in every page, you can create your own class, which will be used in every page, for example:
Response response = new RsXembly(
new XeFoo(this.user)
)
This is how this XeFoo
class would look like:
public final class XeFoo extends XeWrap {
public XeFoo(final String stylesheet, final XeSource... sources) {
super(
new XeAppend(
"page",
new XeMillis(false),
new XeStylesheet(stylesheet),
new XeChain(sources),
new XeSource() {
@Override
public Iterable<Directive> toXembly() {
return new Directives().add("status").set("alive");
}
},
new XeMillis(true)
)
);
}
}
You will need this extra dependency in classpath:
<dependency>
<groupId>com.jcabi.incubator</groupId>
<artifactId>xembly</artifactId>
</dependency>
Here is how we drop a cookie to the user:
public final class TkIndex implements Take {
@Override
public Response act() {
return new RsWithCookie("auth", "John Doe");
}
}
An HTTP response will contain this header, which will place
a auth
cookie into the user's browser:
HTTP/1.1 200 OK
Set-Cookie: auth="John Doe"
This is how you read cookies from a request:
public final class TsIndex implements Takes {
@Override
public Take route(final Request req) {
// the list may be empty
final List<String> cookies = new RqCookies(req).cookie("my-cookie");
}
}
Say, you want to return different content based on Accept
header
of the request (a.k.a. content negotation):
public final class TkIndex implements Take {
@Override
public Response act() {
return new RsFork(
this.request,
new FkTypes("text/*", new RsText("it's a text"))
new FkTypes("application/json", new RsJSON("{\"a\":1}"))
new FkTypes("image/png", /* something else */)
);
}
}
Here is an example of login via Facebook:
new TsAuth(
new TsFork(
new FkRegex("/", new TkHTML("hello, check <a href='/acc'>account</a>")),
new FkRegex("/acc", new TsSecure(new TsAccount()))
),
new PsChain(
new PsCookie(
new CcSafe(new CcHex(new CcXOR(new CcCompact(), "secret-code")))
),
new PsByFlag(
new PsByFlag.Pair(
PsFacebook.class.getSimpleName(),
new PsFacebook("facebook-app-id", "facebook-secret")
),
new PsByFlag.Pair(
PsLogout.class.getSimpleName(),
new PsLogout()
)
)
)
)
Then, you need to show a login link to the user, which he or she can click and get to the Facebook OAuth authentication page. Here is how you do this with XeResponse:
new RsXembly(
new XeStylesheet("/xsl/index.xsl"),
new XeAppend(
"page",
new XeFacebookLink(req, "facebook-app-id"),
// ... other xembly sources
)
)
The link will be add to the XML page like this:
<page>
<links>
<link rel="takes:facebook" href="https://www.facebook.com/dialog/oauth..."/>
</links>
</page>
Similar mechanism can be used for TkGithub
, TkGoogle
, TkLinkedin
, TkTwitter
, etc.
This is how you get currently logged in user:
public final class TkAccount implements Take {
private final Identity identity;
public TkAccount(final Request req) {
this.identity = new RqAuth(req).identity();
}
@Override
public Response act() {
if (this.identity.equals(Identity.ANONYMOUS)) {
// returns "urn:facebook:1234567" for a user logged in via Facebook
this.identity().urn();
}
}
}
There is a convenient class FtCLI
that parses command line arguments and
starts the necessary Front
accordingly.
There are a few command line arguments that should be passed to
FtCLI
constructor:
--port=1234 Tells the server to listen to TCP port 1234
--lifetime=5000 The server will die in five seconds (useful for integration testing)
--refresh Run the server in hit-refresh mode
--daemon Runs the server in Java daemon thread (for integration testing)
--threads=30 Processes incoming HTTP requests in 30 parallel threads
For example:
public final class App {
public static void main(final String... args) {
new FtCLI(
new TsFork(new FkRegex("/", "hello, world!")),
args
).start(Exit.NEVER);
}
}
Then, run it like this:
$ java -cp takes.jar App.class --port=8080 --refresh
You should see "hello, world!" at http://localhost:8080
.
The framework sends all logs to SLF4J logging facility. If you want to see them, configure one of SLF4J bindings.
You are free to use any build tool, but we recommend Maven. This is how your project directory layout may/should look like:
/src
/main
/java
/foo
App.java
/scss
/coffeescript
/resources
/xsl
/js
/css
robot.txt
log4j.properties
/test
/java
/foo
AppTest.java
/resources
log4j.properties
pom.xml
LICENSE.txt