jooby-project / jooby

The modular web framework for Java and Kotlin
https://jooby.io
Apache License 2.0
1.71k stars 197 forks source link

Routes in templates #879

Closed dustContributor closed 7 years ago

dustContributor commented 7 years ago

Hi! In webdev I come from an ASP MVC background so I'm very used to having something like this in my HTML templates:

// Razor template syntax
$.get('@Url.Action("~\Invoice\Read")', function (e) { /*etc*/ };
// Which gets rendered as:
$.get('https://mydomain.com/appname/invoice/read', function (e) { /*etc*/ };
// Or in an api routes object
api.routes = {
  invoice: {
    read: '@Url.Action("~\Invoice\Read")',
    update: '@Url.Action("~\Invoice\Update")',
    delete: '@Url.Action("~\Invoice\Delete")'
  },
  sales:  {
    dailyReport: '@Url.Action("~\Sales\DailyReport")',
    rankedReport: '@Url.Action("~\Sales\RankedReport")'
  }
}
// Rendered as
api.routes = {
  invoice: {
    read: 'https://mydomain.com/appname/invoice/read',
    update: 'https://mydomain.com/appname/invoice/update',
    delete: 'https://mydomain.com/appname/invoice/delete'
  },
  sales:  {
    dailyReport: 'https://mydomain.com/appname/sales/dailyreport',
    rankedReport: 'https://mydomain.com/appname/sales/rankedreport'
  }
}

This is really handy since we use sub-routes to locate our apps (we have lots of apps under the same domain). I dont even have to set the domain nor the sub route in the application config.

Is there some utility function similar in Jooby that I can use from my templates?

jknack commented 7 years ago

Nothing builtin, sorry :(

dustContributor commented 7 years ago

Ah bummer, well since we're at it, any way I can have multiple routes going into the same MVC controller method path?

ie, I want "/" and "/home" to call "index" method in Home controller class.

Also, having the same use case (multiple apps in sub routes inside the same domain), how would I go at publishing Jooby apps like that? Do I need to put everything into a single Jooby project? Because if I deploy a Jooby app, it'll take over a port, so I cant have multiple Jooby apps... Unless they're on different ports and with some (hardware) router magic rules that dispatches the packets to each appropriate application.

jknack commented 7 years ago

ie, I want "/" and "/home" to call "index" method in Home controller class.

@Path({"/", "home"})
@GET
public ... index(...) {
   ...
}

multiple apps in sub routes inside the same domain

Do you mean this? http://jooby.org/doc/#routes-dynamic-advanced-routing

Thanks for using Jooby.

dustContributor commented 7 years ago

Weeell, sort of. Think of it like this:

I have a single server mounted in https://mydomain.com. But actually I serve multiple applications under that domain. I have say, this stock management application for the sales sector of the company, and another different application for in-company news and announcements.

So I have this route for the first app: https://mydomain.com/stockman And this route for the second app: https://mydomain.com/news

The two apps are separate. That probably means two JVM instances each with their Jooby app running. Is this possible? Or I am forced to have both applications running under the same Jooby app instance (thus same JVM instance).

I could start the first app just fine, having its routes listening for "[domain]/stockman", but the second app wont start since the first app is already occupying the default port (8080). Would I need to dispatch requests for each app based on hardware routing? (having one app in port 8080 and another in say, 8081, then work some router magic to route "stockman" requests to 8080 and "news" requests to 8081).

Thanks for using Jooby.

It's pretty nice, I hope you get more users and contributors!

jknack commented 7 years ago

Oh yea, thought you were asking for one app only.

you can set application.port property to 8081 in your .conf file or optionally in your App.java:

{ port(8081); }

dustContributor commented 7 years ago

I see, thanks!

@Path({"/", "home"})

lol I didnt think of trying that :smile:

In any case, given that Jooby itself doesn't knows about it's own host name, since that'd be set in routing rules outside, makes sense that it wont be able to resolve URLs like with @Url.Action("something") in Razor.

I could probably put that information on the config file, and hand it to a template, that generates the URLs with such utility methods in a model instance. I'm using Jade4j templating (development on it seems dead? Really annoying since Jade/Pug is one of the coolest template engines out there).

I was thinking of something along these lines:

public final class HtmlUtil
{
 private final Request req;
 private final Config cfg;
 public HtmlUtil ( Request req, Config cfg )
 {
  this.req = Objects.requireNonNull( req );
  this.cfg = Objects.requireNonNull( cfg );
 }

 public final String resolveUrl ( String url )
 {
  Objects.requireNonNull( url );
  final String host = req..hostname();
  // How do I resolve this? Or how do I get the request URL from the Request object?
  final String protocol = "???"; // http://? https://?
  final String appName = cfg.hasPath( "app.urlName" ) ? cfg.getString( "app.urlName" ) + '/' : "";
  return protocol + host + '/' + appName + url;
 }
}

public final class App extends Jooby
{
 {
  dest.use( "home/index", ( req ) ->
  {
   return Results.html( "views/home/index" ).put( "htmlUtil", new HtmlUtil( req, req.require( Config.class ) ) );
  } );
 }
}

Then in the template:

script.
 App = {
  urls: {
   home: {
    index: "#{htmlUtil.resolveUrl('home/index')}"
   }
  }
 }

But as in the comment, I'm not sure how to gather all the parts needed in 'HtmlUtil#resolveUrl' method. What do you think?

dustContributor commented 7 years ago

Oops, edited the comment, half of it was missing.

jknack commented 7 years ago

jade module isn't dead, but probably needs to be update. Can you file an issue about it?

I didn't get your question, what you showed me looks good.

dustContributor commented 7 years ago

jade module isn't dead, but probably needs to be update. Can you file an issue about it?

Oh I didn't mean your module but the jade4j project. Latest commits were from October 2016, with the author making no commits on anything else for over a year. In any case it seems this week it was revived, couple of fixes were made. Which is nice, it means the author wasn't hit by a bus or something :smile:

I didn't get your question, what you showed me looks good.

Cool, that was the question, if it looked good :smile: Look at the comment there in HtmlUtil#resolveUrl, I'm not sure how to get the protocol used in the route in that context. There is the Request#protocol method but it returns a string like "HTTP/1.1" which isn't very safe, it'd be nicer if it was an enum. What do you think? Something like:

public enum HttpVersion {
  HTTP_1_0( "HTTP/1.0" ),
  HTTPS_1_0( "HTTPS/1.0" ), /* Does this one even exist? */
  HTTP_1_1( "HTTP/1.1" ),
  HTTPS_1_1( "HTTPS/1.1" ),
  HTTP_2_0( "HTTP/2.0" ),
  HTTPS_2_0( "HTTPS/2.0" ); /* Although I'm not sure if there will be un-encrypted HTTP 2.0 */

  public final String name;

  private HttpVersion ( String name ) {
    this.name = Objects.requireNonNull( name );
  }
}

Or say, how do I get the full URL that was used in the request? Request#rawPath or Request#path don't seem to have the full URL used in the browser. I could extract the protocol from the full URL used if I had it.

jknack commented 7 years ago

Enum for protocol sounds like a good addition.

The req.isSecure method defines http vs https. About the url used in the browser, try the host header with req.path()

dustContributor commented 7 years ago

Cool, with that this is what I've ended up with:

public final class HtmlUtil {
  private final Request req;
  private final Config cfg;

  public HtmlUtil ( Request req, Config cfg ) {
    this.req = Objects.requireNonNull( req );
    this.cfg = Objects.requireNonNull( cfg );
  }

  public final String resolveUrl ( String url ) {
    Objects.requireNonNull( url );
    final String host = req.header( "Host" ).value();
    final String protocol = req.secure() ? "https://" : "http://";
    final String appName = cfg.hasPath( "app.urlName" ) ? cfg.getString( "app.urlName" ) + '/' : "";
    return protocol + host + '/' + appName + url;
  }
}

I'm not sure it's terribly robust but oh well :smile:

Thanks!