branaway / japid42

A full feaured Java-based template engine for Play2
57 stars 11 forks source link

h1. japid42 - Japid for Play 2

Bing Ran (bing.ran@gmail.com)

The latest version: see the end of this file for version information

Compatibility: Play 2.1.x, 2.2.x Note: One must use Japid42 version 0.9.16 or later with Play 2.2.1 or later.

Note: version 0.7.4 and older is compatible with 2.0.4. Version 0.9.9.1 or earlier is compatible with Play2.1.x.

h2. About

Japid42 is a native Java based template engine for Java programmers using Play2. It can also be used in any application that need an advanced templating solution.

Very fast in reloading changed views. You simply change the views and refresh your browser to get it right away in a blink instead of taking a coffee break. Japid manages its own view compilation and class loading. Changes in the Japid views are isolated from the Play reloading mechanism.

Full featured. It at least matches the feature set of the built-in Scala based template engine.

Excellent runtime performance.

Java-based, thus very friendly to Java programmers.

The core Japid has been used in production for a long time and is very robust.

Japid views compile lazy and compile on demand, unnoticed.

It integrates with Play2 seamlessly, in the meantime can be used stand-alone with any Java applications.

h2. Features

New features since version 0.9.5:

Credit: the work is derived from Peter Hausel's little nice project hosted here: https://github.com/pk11/play-jaxrs. The differences are explained later.

h2. Usage

h4. For a full sample application with Japid, see: http://github.com/branaway/computer-japid

Please also check out the JapidSample project in the samples directory for an example.

Basically you'll need to do three things:

  1. Get the dependencies

Note: adjust the version number accordingly.

  1. Initialize Japid in Global.java in the app directory:
public class Global extends cn.bran.play.GlobalSettingsWithJapid{}

Note:

    addImport(String imp)
    addImport(Class cls)
    addImportStatic(String imp)
    addImportStatic(Class cls)
    setKeepJavaFiles(boolean keepJava)
    setLogVerbose(boolean verb)
    setTemplateRoot(String... root)
    setRefreshInterval(int i)
    setCacheResponse(boolean c)
    setPresentErrorInHtml(boolean presentErrorInHtml)
    setEnableJITCachePersistence(boolean enableJITCachePersistence)

Please see the @computer-japid@ (https://github.com/branaway/computer-japid) example for a real-world Global class definition.

  1. Extend cn.bran.play.JapidController to create your own controllers and use the renderJapid(...) etc to render the views.
package controllers;

import play.mvc.Result;
import cn.bran.play.JapidController;

public class Application extends JapidController {

  public static Result index() {
      return renderJapid("cool");
  }
}
  1. Create the Japid view script "{my app root}/japidroot/japidviews/Application/index.html":
@(String m)
Hello, $m!
  1. Hit http://localhost:9000/ to see Japid at work!

h2. Releasing Your Application

When an app is ready to be distributed, you'll need to modify the build.scala to prepare your project for distribution. There are two things needed to be done:

include a directive in the build.scala to specify where the Japid root directory is, so the Japid scripts are included in the generated jar file. Note, you'll want to leave it commented out in dev mode, since it would trigger app reloading when a Japid script is changed.


    val main = play.Project(appName, appVersion, Seq()).settings(
        ...
        ,unmanagedResourceDirectories in Compile  <+= baseDirectory( _ / "japidroot" )
    )

(optionally, to avoid some warning messages) set the japid root to null explicitly in your global settings file, so the Japid engine will pick up the Japid scripts in the jars.


    public class Global extends GlobalSettingsWithJapid {
        @Override
        public void onStartJapid() {
            if (_app.isDev())
                setTemplateRoot("japidroot");
            else
                setTemplateRoot((String[])null); // scan class only
        }
    }

This applies to using Japid in sub-projects settings too.

h2. Using Japid in Sub-projects

In Build.scala, you may have a sub-project defined like this:


    val appDependencies = Seq(
      "japid42" % "japid42_2.10" % "{the version number}"
    )

    val foo = PlayProject("foo", "0.1", appDependencies, path = file("modules/foo"))

    val main = PlayProject(appName, appVersion, appDependencies).settings(
      resolvers += Resolver.url("Japid on Github", url("http://branaway.github.io/releases/"))(Resolver.ivyStylePatterns)
    ).dependsOn(foo)

Obviously you have a sub-project located in modules/foo. For the system to know the Japid scripts in the sub-project, you'll need to tell Japid the location, using the JapidRenderer.setTemplateRoot(String...) method, in the global initialization method:


public class Global extends cn.bran.play.GlobalSettingsWithJapid {
    @Override
    public void onStartJapid() {
        setTemplateRoot("japidroot", "modules/foo/japidroot");
    }
}

In the above example, the first argument is the location of the Japid scripts in the parent project. The second argument is the root for the Japid scripts in the sub-project. All paths are relative to the root of the master project.

The @computer-japid@(http://github.com/branaway/computer-japid) sample application demos this feature.

h2. Use Japid Router to replace the Play2 routing mechanism (experimental but quite usable)

Note: version 0.9.5 supports Play 2.1.1 while version 0.9.6+ supports Play 2.1.2, and version 0.9.11 supports Play 2.2.x.

Every typical Play applications come with a route file in the conf directory. The conf file is compiled to multiple scala files when the app is loaded or reloaded.

Two issues with the approach:

Compiling the route file is slow. The multiple scala files derived from the route file take a long time a compile. Any change in the route file triggers this lengthy process which is a productivity killer in development.

Maintaining the route file is annoying. Every time one adds a new method or change the signature of an action method, one must go searching the long routing table in the route file to adjust the mapping properly. Another productivity killer.

The newly introduced Japid Router takes care of these by using some conventions with minimum Annotations.

These are the steps to take advantage of the feature:

  1. Enable the Japid Router in the global settings:
public class Global extends GlobalSettingsWithJapid {
    @Override
    public void onStartJapid() {
        //...
        setUseJapidRouting(true); // not required since 0.9.11
    }
}
  1. Use a few annotations in the controllers:
package controllers;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;

import play.mvc.Result;
import cn.bran.play.JapidController;

@Path("") // an empty value means to use the controller name (minus the leading "controller." part) as the first segment of the URL path
public class Jax extends JapidController {
    // takes HTTP GET. the path is "yahoo"
    @GET
    public static Result yahoo() {
        return ok("news!");
    }

    @GET
    @Path("/glass") // explicit path
    public static Result g() {
        return ok(renderJapid("ggff"));
    }

    // take any HTTP method
    // explicit path following JAX-RS convention
    @Path("/book/{isbn:[0-9]+}/{chapter:[abc]}")
    public static Result getBook(@PathParam("isbn") Integer id, @PathParam("chapter") String chap, @QueryParam("note") String note) {
        return ok("got book: " + id + "/" + chap + ": " + note);
    }

    // take any HTTP method
    // auto-routing based on positional parameter mapping
    public static Result bo(int id, String chap, @QueryParam("note") String note) {
        return ok("got bo: " + id + "/" + chap + ": " + note);
    }
}

Note: the Japid routing can actually co-exist with the traditional route file. The Japid router falls back to the default route file in case no route is matched.

The @GET, @POST, @PUT, @DELETE, @HEAD, @OPTIONS, @Path, @PathParam and @QueryParam are standard JAX-RS annotations. But the resemblance to JAX-RS ends there.

h3. The auto-routing convention

When an action method carries no JAX-RS @Path annotation, the routing takes a set of conventional rules:

@PathParam CANNOT be used with the parameters whereas @QueryParam can still be used.

A URL is composed in multiple segments separated by a "/".

The first segment is the controller's class name, minus the "controller." part, plus the name of the method to invoke.

The rest of the URL path is mapped to the parameters of the action method by position, i.e., the first segment is mapped to the first none-@QueryParam-annotated parameter and so on. Basically the URL is composed to invoke the action method as in PASCAL convention. An example:


package controllers;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import play.mvc.Result;
@Path("") // the path to this controller is the name of the class minus the "controller." part
public class Jax extends Controller {
    public static Result book(int id, String chap, @QueryParam("note") String note) {
        return ok("got book: " + id + "/" + chap + ": " + note);
    }
}

To invoke the @book@ action, the URL must be compose similar to @/Jax.book/123/chapter2?note=mynote@. The id parameter takes 123 as the value. The chap parameter takes "chapter2" as the value. The argument to the "note" parameter gets its value from the query string in the name of "note". The convention can be summarized as

http://myhost/{app name}/{controller name without leading "controllers."}.{method name}/{parameter 1}/{parameter 2}/...?{query param 1}=...&{query param 2}=...

Note: the app name segment is defined by an instance of @ApplicationPath with the Global class.

h3. Credit

As mentioned before, the Japid router is inspired by Peter Hausel's project at "https://github.com/pk11/play-jaxrs". I have made a few interesting features on top of it.

Automatic argument type conversion was added.

Regex format of path parameters are enforced.

The use HTTP method annotation, @Path, @PathParam are all optional, and in that case we use some conventions to route the HTTP requests, as explained above.

The implementation is mostly Java based.

h2. Use Japid in any Java applications

First of all, you grab the japid jar from the @target/scala-2.10@ folder and two other dependencies from the @libs4plainjava@ folder.

Then the following code snippet is the simplest form of using Japid in any plain Java applications:

    // do this once, using true to set dev mode to reload Japid script changes, false otherwise. 
JapidRenderer.init(true) 
    // now render some data to a Japid script "{app name}/japidroot/japidviews/hello.html"
RenderResult rr = JapidRenderer.renderWith("japidviews/hello.html", "John");
    // rr.toString() outputs what you want to see

By default the intermediary Java files derived from the Japid scripts are saved along with the scripts. Use @JapidRenderer.setKeepJavaFiles(false)@ to keep them in the memory only.

The following is a code snippet that demonstrate using Japid with a Servlet:

public class S2 extends HttpServlet {

    public void init(ServletConfig config) throws ServletException {
        String p = config.getServletContext().getRealPath("WEB-INF/japidroot");
        JapidRenderer.setTemplateRoot(p);
        JapidRenderer.init(true);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Person p = new Person(); p.name = "Nadal";
        RenderResult rr = JapidRenderer.render(p);
        response.getWriter().println(rr.toString());
    }

}

h2. Documentation

The "computer-japid" sample project contains a README file that explains most of the Japid usage. Please refer to it.

For detailed Japid grammar please refer to the Japid 1 manual before I write something formal:

https://github.com/branaway/Japid

h2. Sample applications

h2. Hacking Japid42 in Eclipse

  1. Git clone the Japid42 project to your local computer.
  2. Build it and publish it to your local repository: "play publish-local", where the play is the Play2 binary.

Note:

h2. History

2012.3.24: v0.1

initial commit.

2012.3.27: v0.2

lots of fix and the JapidSample mostly works now.

2012.4.4: v0.2.1

added JapidRenderer.setLogVerbose() to control logging verbosity of Japid activities.

2012.7.28: v0.2.2

updated the README and fixed the initialization part.

2012.7.28: v0.3

bug fix: import of empty _tags or _layouts caused compiler error. Now imports those packages only if there are any templates in the folders.

enhancement: updated build files to work with play-2.0.3 release.

2012.10.12. v0.4, beta

made it play 2.0.4 compatible

major bug fix: class reloading on the controller side or the model side caused type mismatched problem.

2012.10.17. v0.5

major feature: added nice error reporting.

bug fix: htmlentities.properties file moved to conf

2012.10.23. v0.5.1

enhancement: hide the template for dev error reporting from users. The side effect is users cannot customize the error reporting at dev time.

removed: removed _javatags, _errors special directories in japidviews folder.

added: ported some util methods from Play 1's JavaExtensions to WebUtils.

enhancement: merged Yuvi Masory's JDT compiler dependency change

2012.10.23. v0.5.2

bug fix: error reporting of argument mismatch in calling Japid views from controller.

enhancement: added toString() to JapidResult so it can be used directly in Japid views as the result of directly calling a controller method. For an example, one can include the result of an action call directly in a script like this:

<code>the value is $MyController.action(...).</code>

2012.11.1. v0.5.3

enhancement: since I started using the Github pages as the repository, the readme file started referencing the resolver to it instead of local.

2012.11.8. v0.5.4

new feature: added JapidRenderer.setTemplateRoot(String...) that takes one or more japid roots. With this support, Japid can be used in project/sub-project layout.

2012.11.10. v0.5.5

bug fix: failed to create folders matching controllers in mkdir()

critical bug fix: play start did not work. Class was not found.

simplification: _layout and _tags were deprecated. Put shared stuff in the japidviews dir or configure import manually.

2012.11.10. v0.6

enhancement: in dev mode, added compile on demand with delay, so that Japid script refactory is more reliable.

2012.11.10. v0.6.1

bug fix: detected abstract directive and throw exception in apply() in java code

bug fix: noplay code contained old t variable

bug fix: inner classes rendered false change alarm leading to excessive recompiling in dev mode.

2012.11.18. v0.7. See the JapidSample42 for examples for the new features.

new feature: cached action invocation supported. One must create a Global class that inherits from JapidRender instead of directly from GlobalSettings. The annotation of Cached was activated. With JapidSample42, see: http://localhost:9000/more.Portlets/index

new feature: security token now supported. With JapidSample42, see: http://localhost:9000/application/dumpPost/a/bb/ccc

2012.11.20. v0.7.1

enhancement: the @authenticity token@ mechanism was remodeled and simplified. Two steps to combat cross-site forged form attach:

- Annotate the form class with @cn.bran.play.AuthenticForm@
- embed $authenticityToken() in the proper position of an html form in your Japid templates. The method would generate a hidden field in the form. 

2012.11.26. v0.7.2

new feature: JapidRenderer.setKeepJavaFiles(boolean) enabled users to hide all the Java files during the process of Japid script compilation. Default is true to keep the compatibility with old version. When set to false, this configuration is faster in scanning changed files.

2012.11.27. v0.7.3

removed: no logging in the template classloader any more. Solid enough to turn it off.

2012.12.2. v0.7.4

added JapidRenderer.registerTemplate(...): it's intended for use at runtime to dynamically add new Japid script to the engine.

enhancement: reduced file system access while refreshing Japid template classes.

2013.2.8. v0.8

changed for Play 2.1.0 compatibility

2013.2.8. v0.8.1

new feature: using dynamic template in memory. Now one can render data to a template in the form of Java String in memory. The template can be retrieved from a database storage for example:


// an action in a controller
    public static Result dynamic() {
        String template = "`(String s, int i)\nhi ${s}, are you ${i} years old?";
        return ok(renderDynamic(template, "Johnny", 13));
    }

2013.5.15. v0.9.1

  1. breaking change: re-factored JapidRenderer to Play independent. Added GlobalSettingsWithJapid for users to extend from when creating the Global configurations:

  public class Global extends cn.bran.play.GlobalSettingsWithJapid {
        @Override
        public void onStartJapid() {
            setTemplateRoot("japidroot", "modules/foo/japidroot");
            setLogVerbose(true);
            setKeepJavaFiles(false); // keep the Java code derived from Japid scripts in memory only
            addImport("com.foo");
        }
  }
  1. New feature: now the JapidRenderer can be used out of Play to render any data to Japid scripts. e.g.:

    @Test
    public void testDynamic() throws IOException {
        JapidRenderer.setKeepJavaFiles(false);
        JapidRenderer.init(OpMode.dev, "japidroot", 1, null, JapidRenderer.class.getClassLoader());

        String template = "`(String s, int i)\n";
        template +="hi ${s}, are you ${i} years old?";
        template +="\n`t japidviews._tags.taggy `-"; // calling a tag on hard drive
        RenderResult rr = JapidRenderer.renderDynamic(template, "Johnny", 13);
        System.out.println(rr);
        rr = JapidRenderer.renderWith("japidviews/myscript.html", "hi", 123);
        System.out.println(rr);

    }

Pay attention to the renderDynamic(...) and renderWith(...) methods.

  1. new feature: support JDK 7 syntax in Japid script.e.g.:

    
    `(String who)
    Hello ${who}!
    ` switch(who) { case "John":
        this is John
    ` break; case "Mary":
        this is Mary
    ` break; default:
        who are you?
    `}
    
  2. various bug fixes to pass all the unit tests

2013.5.23 v0.9.2

performance enhancement: removed the overhead associated with finding the matching template in renderJapid(...)

change: lowered the required play version to 2.1.0 in the plugins.sbt.

2013.5.28 v0.9.2.3

bug fix: called init after the onStartJapid() in GlobalSettingsWithJapid

work around: heroku environment does not like custom header without "X-". Modified to "X-Japid..."

2013.5.29 v0.9.3

fixed readme and bumped up the version to 0.9.3

2013.6.9 v0.9.3 (keep the old number)

fixed: there was transitive dependency on play in the JapidAbstractCompiler. Removed it so stand-alone use of Japid proceeds as claimed.

2013.6.10 v0.9.4

fixed: JapidRenderer.init() ignored previously set template root.

enhanced: added Servlet sample in the readme to show how to use Japid in servlets.

2013.6.13 v0.9.4.1

fixed: trouble finding java source with dot on the path.

enhanced: JIT cache persisting. cached the Japid classes once a new compile is done. Used to be an explicit call. For use in stand-alone environment such as Tomcat.

2013.7.18 v0.9.5

new feture: Japid script can be packaged in jars. Japid scans resources in the path of "japidviews/" in jar files and compile all the found Japid scripts. To pack Japid scripts in a jar file from a Play project, add the folowing line to the PlayProject settings(assuming your japid scripts are rooted in "japidroot" directory):

  unmanagedResourceDirectories in Compile  <+= baseDirectory( _ / "japidroot" )

new feature: routing without the route files. a big feature to replace the default route file with annotations and conventions.

2013.7.23 0.9.7

new feature: added @OptionalExt in the routing package to specify an optional path extension to an action. With the annotation, "/controller/action/p1.html" equals "/controller/action/p1".

2013.8.30 0.9.8

fix: &{xxx, "yyy", zzz} did not work because old code did not allow more than one args

2013.10.3 0.9.9

bug: cannot run app from a jar distribution. One way of running a Play 2 app is to run the "play dist" and run the start script packged in the zip file generated in the dist commmand. There are two things to do to make it work:

include a directive in the build.scala:


    val main = play.Project(appName, appVersion, Seq()).settings(
        ...
        ,unmanagedResourceDirectories in Compile  <+= baseDirectory( _ / "japidroot" )
    )

(optionally) set the japid root to null explicitly in your global settings file:


    public class Global extends GlobalSettingsWithJapid {
        @Override
        public void onStartJapid() {
            if (_app.isDev())
                setTemplateRoot("japidroot", "app");
            else
                setTemplateRoot((String[])null); // scan class only
        }
    }

bug: user defined imported were not added to the diervied Java code for those Japid scripts that were picked up from classpath.

2013.10.3 0.9.9.1

enhancement: previously a runtime exception was thrown when the japid roots were not found. Changed to issue an warning, so that applications will continue to pick up Japid scripts from classpath. This would make the second step in the 0.9.9 bug fix optional.

2013.10.6 version 0.9.10

API upgrade: initial support for Play 2.2.0. internal API change to comply with the API changes in play 2.2.0, mainly the Result class hierarchy.

2013.10.9 version 0.9.10.1

bug fix: the cache mechnism in compiling japid scripts from classpath were overly optimistic, causing new scripts not picked up. Removed the cache for now.

enhancement: japid scripts from classpath got recompiled everytime being called again. Now the contributor is checked to avoid japid scripts from classpath being recompiled too often.

2013.10.11 version 0.9.11

bahavior change: set the default value on to turn on the jax style annotation based routing.

bahavior change: when in auto-routing, class name and method name are now connected by a dot ".". It was a slash.

bug fix: a bug was introduced in 0.9.10.1 that created NPE in some situations.

bug fix: An app would always compile the Japid on each request, when set to keep Japid Java files on disk.

bug fix: Japid scripts contained in jars were not picked after an app reloading.

2013.10.15 version 0.9.12

enhancement: routes dervied from jax-rs annotations were added to the 404 page in dev mode.

enhancement: japid cached would get cleared if the cache file was deemed too old (about 20 mins)

2013.10.29 version 0.9.13

bug fix: callback param list of tag invocation did not allow generics

bug fix: application.conf has been wrongly packged in the distribution. It was removed since.

2013.10.31 version 0.9.13.1

repository change: github has changed the root domain for the "pages" feature from "github.com" to "github.io". Any apps using Japid must change the japid repository domain from branaway.github.com to branaway.github.io, otherwise a "dependency cannot be resolved" error is due.

2013.11.14 version 0.9.14

play compatibility upgrade: compiled against play 2.2.1

2013.11.24 version 0.9.16

bug fix: asset serving in JaxRouter did not work well in packaged distribution (jar files). Removed the logic.

API change for performance: the "lang" variable in template was deprecated because it would cost a ~100 micro-seconds CPU time. Use "Implicit.lang()" to get it explicitly.

enhancement: added japid version header to the generated Java files so Future Japid upgrade would invalidate the Java artifacts from older Japid versions.

performance enhancement (from Japid 1): tag calling performance was improved by removing unnecessary initiation code in tag object initiation.

2013.12.21 version 0.9.16.1

enhancement: to detect scripts with a future time stamp and reset them to current time so that newly generated java files would be always newer.

2014.3.11 version 0.9.17

bug fix: reflection did not detect added or removed controllers. See the new JaxrsRouter#getControllersWithPath()

bug fix: added source and target spec for 1.6 in the build.scala

2014.3.11 version 0.9.17.1

perf enhancement: avoided version checks while programmatic reloading when japid source project was used as a module.