Blog from July, 2017

Some improvements I've just added surrounding the menu-item widgets:

  • Widget classes can now be defined as inner-classes of resources.
  • New Tooltip template class has been added that allows you to programmatically specify a tooltip.  It can be mixed with arbitrary HTML5 beans.

I've added a new menu-item example to the Pet Store resource complete with tooltips:

 

The menu item is defined as an inner class on the PetStoreResource class and gets added via a new addPet() method...

/**
 * Sample REST resource that renders summary and detail views of the same bean.
 */
@RestResource(
   ...
)
public class PetStoreResource extends ResourceJena {

	@RestMethod(name="POST", path="/")
	public Redirect addPet(@Body Pet pet) throws Exception {
		this.petDB.put(pet.id, pet);
		return new Redirect("servlet:/");
	}
	
	/**
	 * Renders the "ADD" menu item.
	 */
	public class AddPet extends MenuItemWidget {

		@Override
		public String getLabel(RestRequest req) throws Exception {
			return "add";
		}

		@Override
		public Object getContent(RestRequest req) throws Exception {
			return div(
				form().id("form").action("servlet:/").method("POST").children(
					table(
						tr(
							th("ID:"),
							td(input().name("id").type("number").value(getNextAvailableId())),
							td(new Tooltip("(?)", "A unique identifer for the pet.", br(), "Must not conflict with existing IDs"))
						),
						tr(
							th("Name:"),
							td(input().name("name").type("text")),
							td(new Tooltip("(?)", "The name of the pet.", br(), "e.g. 'Fluffy'")) 
						),
						tr(
							th("Kind:"),
							td(
								select().name("kind").children(
									option("CAT"), option("DOG"), option("BIRD"), option("FISH"), option("MOUSE"), option("RABBIT"), option("SNAKE")
								)
							),
							td(new Tooltip("(?)", "The kind of animal.")) 
						),
						tr(
							th("Breed:"),
							td(input().name("breed").type("text")),
							td(new Tooltip("(?)", "The breed of animal.", br(), "Can be any arbitrary text")) 
						),
						tr(
							th("Gets along with:"),
							td(input().name("getsAlongWith").type("text")),
							td(new Tooltip("(?)", "A comma-delimited list of other animal types that this animal gets along with.")) 
						),
						tr(
							th("Price:"),
							td(input().name("price").type("number").placeholder("1.0").step("0.01").min(1).max(100)),
							td(new Tooltip("(?)", "The price to charge for this pet.")) 
						),
						tr(
							th("Birthdate:"),
							td(input().name("birthDate").type("date")),
							td(new Tooltip("(?)", "The pets birthday.")) 
						),
						tr(
							td().colspan(2).style("text-align:right").children(
								button("reset", "Reset"),
								button("button","Cancel").onclick("window.location.href='/'"),
								button("submit", "Submit")
							)
						)
					).style("white-space:nowrap")
				)
			);
		}
	}
	
	// Poormans method for finding an available ID.
	private int getNextAvailableId() {
		int i = 100;
		for (Integer k : petDB.keySet())
			i = Math.max(i, k);
		return i+1;
	}
}

 

The Tooltip class is a simple POJO with a swap method that wraps the contents in a Div tag....

public class Tooltip {
	private final String display;
	private final List<Object> content;

   /**
    * Constructor.
    *
    * @param display
    * 	The normal display text.
    * 	This is what gets rendered normally.
    * @param content
    * 	The hover contents.
    * 	Typically a list of strings, but can also include any HTML5 beans as well.
    */
   public Tooltip(String display, Object...content) {
   	this.display = display;
   	this.content = new ArrayList<Object>(Arrays.asList(content));
   }

   /**
    * The swap method.
    *
    * <p>
    * Converts this bean into a div tag with contents.
    *
    * @param session The bean session.
    * @return The swapped contents of this bean.
    */
   public Div swap(BeanSession session) {
      return div(
      	small(display),
      	span()._class("tooltiptext").children(content)
      )._class("tooltip");
   }
}

This is a notification of changes being made internally in the serializers and parsers.  Existing APIs and behavior are unchanged.

The major change is that SerializerSession and ParserSession objects are now reusable as long as they're reused within the same thread.  They used to be throw-away objects that would only exist for the duration of a single serialization/parse.

// Old way (still works)
JsonSerializer.DEFAULT.serialize(writer1, pojo1);
JsonSerializer.DEFAULT.serialize(writer2, pojo2);

// New way now possible
SerializerSession session = JsonSerializer.DEFAULT.createSession();
try {
  session.serialize(writer1, pojo1);
  session.serialize(writer2, pojo2);
} finally {
session.close();
}  
 

The original driver behind this change was so that the existing HtmlSerializerSession can be used in the MenuItemWidget (and other widgets) to serialize POJOs to match the formatting rules on the rest of the page. 

Internally, the actual serialization and parsing code has been moved from the Serializer/Parser subclasses into the Session classes.  This had the nice side effect of allowing me to eliminate many of the getter methods on the session objects and replace them with references to private final fields which cleaned up the code quite a bit.  There might be some slight performance improvements, but the GIT compiler was probably already inlining much of this code.

Session objects are rather cheap to create, but they do still involve setting 20+ private final fields.  So I can envision the sessions being reused when ultra-efficiency is needed.  But again the usecase would be limited since they're not designed to be thread safe.

 

Objective: 

Bind a specific POJO to a freemarker template and render the object as an html element.

Steps:

First, your project should include freemarker:

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.26-incubating</version>
</dependency>

Next, you need a proper pojo containing multiple fields. Here's the pojo I'm trying to customize: 

Adr.java
import java.io.Serializable;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.juneau.annotation.BeanProperty;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Generated("org.jsonschema2pojo")
@JsonPropertyOrder({
    "addr1",
    "addr2",
    "country",
    "city",
    "state",
    "zip"
})
public class Adr implements Serializable
{

    @JsonProperty("addr1")
    @BeanProperty("addr1")
    private String addr1;
    @JsonProperty("addr2")
    @BeanProperty("addr2")
    private String addr2;
    @JsonProperty("country")
    @BeanProperty("country")
    private String country;
    @JsonProperty("city")
    @BeanProperty("city")
    private String city;
    @JsonProperty("state")
    @BeanProperty("state")
    private String state;
    @JsonProperty("zip")
    @BeanProperty("zip")
    private String zip;

    /**
     * 
     * @return
     *     The addr1
     */
    @JsonProperty("addr1")
    @BeanProperty("addr1")
    public String getAddr1() {
        return addr1;
    }

    /**
     * 
     * @param addr1
     *     The addr1
     */
    @JsonProperty("addr1")
    @BeanProperty("addr1")
    public void setAddr1(String addr1) {
        this.addr1 = addr1;
    }

    public Adr withAddr1(String addr1) {
        this.addr1 = addr1;
        return this;
    }

    /**
     * 
     * @return
     *     The addr2
     */
    @JsonProperty("addr2")
    @BeanProperty("addr2")
    public String getAddr2() {
        return addr2;
    }

    /**
     * 
     * @param addr2
     *     The addr2
     */
    @JsonProperty("addr2")
    @BeanProperty("addr2")
    public void setAddr2(String addr2) {
        this.addr2 = addr2;
    }

    public Adr withAddr2(String addr2) {
        this.addr2 = addr2;
        return this;
    }

    /**
     * 
     * @return
     *     The country
     */
    @JsonProperty("country")
    @BeanProperty("country")
    public String getCountry() {
        return country;
    }

    /**
     * 
     * @param country
     *     The country
     */
    @JsonProperty("country")
    @BeanProperty("country")
    public void setCountry(String country) {
        this.country = country;
    }

    public Adr withCountry(String country) {
        this.country = country;
        return this;
    }

    /**
     * 
     * @return
     *     The city
     */
    @JsonProperty("city")
    @BeanProperty("city")
    public String getCity() {
        return city;
    }

    /**
     * 
     * @param city
     *     The city
     */
    @JsonProperty("city")
    @BeanProperty("city")
    public void setCity(String city) {
        this.city = city;
    }

    public Adr withCity(String city) {
        this.city = city;
        return this;
    }

    /**
     * 
     * @return
     *     The state
     */
    @JsonProperty("state")
    @BeanProperty("state")
    public String getState() {
        return state;
    }

    /**
     * 
     * @param state
     *     The state
     */
    @JsonProperty("state")
    @BeanProperty("state")
    public void setState(String state) {
        this.state = state;
    }

    public Adr withState(String state) {
        this.state = state;
        return this;
    }

    /**
     * 
     * @return
     *     The zip
     */
    @JsonProperty("zip")
    @BeanProperty("zip")
    public String getZip() {
        return zip;
    }

    /**
     * 
     * @param zip
     *     The zip
     */
    @JsonProperty("zip")
    @BeanProperty("zip")
    public void setZip(String zip) {
        this.zip = zip;
    }

    public Adr withZip(String zip) {
        this.zip = zip;
        return this;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(addr1).append(addr2).append(country).append(city).append(state).append(zip).toHashCode();
    }

    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if ((other instanceof Adr) == false) {
            return false;
        }
        Adr rhs = ((Adr) other);
        return new EqualsBuilder().append(addr1, rhs.addr1).append(addr2, rhs.addr2).append(country, rhs.country).append(city, rhs.city).append(state, rhs.state).append(zip, rhs.zip).isEquals();
    }

}

 

Next, create a freemarker template to translate an instance of this bean type into a specific HTML element.

Here's an example that renders the address class as a DIV, using a conditional on the addr2 field, and combining city state and zip into a single row.

MemberAddressRender.div.html
<div>
    <table>
        <tr>
            <td>
            ${addr1}
            </td>
        </tr>
        <#if addr2?has_content>
        <tr>
            <td>
            ${addr2}
            </td>
        </tr>
        </#if>
        <tr>
            <td>
            ${city}, ${state}  ${zip}
            </td>
        </tr>
    </table>
</div>

 

Next, you need to create an HtmlRender class for this type of bean.  For example:

MemberAddressRender.java
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import org.apache.juneau.dto.html5.Div;
import org.apache.juneau.dto.html5.Form;
import org.apache.juneau.html.HtmlParser;
import org.apache.juneau.html.HtmlRender;
import org.apache.juneau.serializer.SerializerSession;
import Adr;

import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.Locale;

import static org.apache.juneau.dto.html5.HtmlBuilder.div;
import static org.apache.juneau.dto.html5.HtmlBuilder.h2;

/**
 * Created by sblackmon on 7/17/17.
 */
public class MemberAddressRender extends HtmlRender<Adr> {

  static Configuration cfg = new Configuration(Configuration.VERSION_2_3_26);

  static {
    cfg.setClassLoaderForTemplateLoading(MemberAddressRender.class.getClassLoader());
    cfg.setEncoding(Locale.US, Charset.defaultCharset().name());
    cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
  }

  @Override
  public Div getContent(SerializerSession session, Adr value) {
    StringWriter stringWriter = new StringWriter();
    try {
      Template myTemplate = cfg.getTemplate("MemberAddressRender.div.ftl");
      myTemplate.process(value, stringWriter);
    } catch( Exception ex ) {
      ex.printStackTrace();
    }
    String divString = stringWriter.toString();
    Div div = div();
    try {
      div = HtmlParser.DEFAULT.parse(divString, Div.class);
    } catch( Exception ex ) {
      ex.printStackTrace();
    }
    return div;
  }
}

 

Finally, you need to bind the custom Render class onto the class(es) which have the class you are customizing as a child.

MemberHtml.java
import org.apache.juneau.html.annotation.Html;
import Adr;
import Member;

/**
 * Created by sblackmon on 7/17/17.
 */

public class MemberHtml extends Member {

  @Override
  @Html(link = "servlet:/{username}")
  public String getUsername() {
    return super.getUsername();
  }

  @Override
  @Html(render=MemberAddressRender.class)
  public Adr getAdr() {
    return super.getAdr();
  }

  @Override
  @Html(render=MemberImageRender.class)
  public String getPicture() {
    return super.getPicture();
  }
}

In case you are interested, here's the auto-generated Member class whose presentation is being customized.

Member.java
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import javax.validation.Valid;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.juneau.annotation.BeanProperty;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Generated("org.jsonschema2pojo")
@JsonPropertyOrder({
    "username",
    "email",
    "picture",
    "adr",
    "bday",
    "tel",
    "title",
    "url",
    "skills",
    "profiles"
})
public class Member implements Serializable
{

    @JsonProperty("username")
    @BeanProperty("username")
    private String username;
    @JsonProperty("email")
    @BeanProperty("email")
    private String email;
    @JsonProperty("picture")
    @BeanProperty("picture")
    private String picture;
    @JsonProperty("adr")
    @BeanProperty("adr")
    @Valid
    private Adr adr;
    @JsonProperty("bday")
    @BeanProperty("bday")
    private String bday;
    @JsonProperty("tel")
    @BeanProperty("tel")
    private String tel;
    @JsonProperty("title")
    @BeanProperty("title")
    private String title;
    @JsonProperty("url")
    @BeanProperty("url")
    private String url;
    @JsonProperty("skills")
    @BeanProperty("skills")
    @Valid
    private List<Skill> skills = new ArrayList<Skill>();
    @JsonProperty("profiles")
    @BeanProperty("profiles")
    @Valid
    private Profiles profiles;

    /**
     * 
     * @return
     *     The username
     */
    @JsonProperty("username")
    @BeanProperty("username")
    public String getUsername() {
        return username;
    }

    /**
     * 
     * @param username
     *     The username
     */
    @JsonProperty("username")
    @BeanProperty("username")
    public void setUsername(String username) {
        this.username = username;
    }

    public Member withUsername(String username) {
        this.username = username;
        return this;
    }

    /**
     * 
     * @return
     *     The email
     */
    @JsonProperty("email")
    @BeanProperty("email")
    public String getEmail() {
        return email;
    }

    /**
     * 
     * @param email
     *     The email
     */
    @JsonProperty("email")
    @BeanProperty("email")
    public void setEmail(String email) {
        this.email = email;
    }

    public Member withEmail(String email) {
        this.email = email;
        return this;
    }

    /**
     * 
     * @return
     *     The picture
     */
    @JsonProperty("picture")
    @BeanProperty("picture")
    public String getPicture() {
        return picture;
    }

    /**
     * 
     * @param picture
     *     The picture
     */
    @JsonProperty("picture")
    @BeanProperty("picture")
    public void setPicture(String picture) {
        this.picture = picture;
    }

    public Member withPicture(String picture) {
        this.picture = picture;
        return this;
    }

    /**
     * 
     * @return
     *     The adr
     */
    @JsonProperty("adr")
    @BeanProperty("adr")
    public Adr getAdr() {
        return adr;
    }

    /**
     * 
     * @param adr
     *     The adr
     */
    @JsonProperty("adr")
    @BeanProperty("adr")
    public void setAdr(Adr adr) {
        this.adr = adr;
    }

    public Member withAdr(Adr adr) {
        this.adr = adr;
        return this;
    }

    /**
     * 
     * @return
     *     The bday
     */
    @JsonProperty("bday")
    @BeanProperty("bday")
    public String getBday() {
        return bday;
    }

    /**
     * 
     * @param bday
     *     The bday
     */
    @JsonProperty("bday")
    @BeanProperty("bday")
    public void setBday(String bday) {
        this.bday = bday;
    }

    public Member withBday(String bday) {
        this.bday = bday;
        return this;
    }

    /**
     * 
     * @return
     *     The tel
     */
    @JsonProperty("tel")
    @BeanProperty("tel")
    public String getTel() {
        return tel;
    }

    /**
     * 
     * @param tel
     *     The tel
     */
    @JsonProperty("tel")
    @BeanProperty("tel")
    public void setTel(String tel) {
        this.tel = tel;
    }

    public Member withTel(String tel) {
        this.tel = tel;
        return this;
    }

    /**
     * 
     * @return
     *     The title
     */
    @JsonProperty("title")
    @BeanProperty("title")
    public String getTitle() {
        return title;
    }

    /**
     * 
     * @param title
     *     The title
     */
    @JsonProperty("title")
    @BeanProperty("title")
    public void setTitle(String title) {
        this.title = title;
    }

    public Member withTitle(String title) {
        this.title = title;
        return this;
    }

    /**
     * 
     * @return
     *     The url
     */
    @JsonProperty("url")
    @BeanProperty("url")
    public String getUrl() {
        return url;
    }

    /**
     * 
     * @param url
     *     The url
     */
    @JsonProperty("url")
    @BeanProperty("url")
    public void setUrl(String url) {
        this.url = url;
    }

    public Member withUrl(String url) {
        this.url = url;
        return this;
    }

    /**
     * 
     * @return
     *     The skills
     */
    @JsonProperty("skills")
    @BeanProperty("skills")
    public List<Skill> getSkills() {
        return skills;
    }

    /**
     * 
     * @param skills
     *     The skills
     */
    @JsonProperty("skills")
    @BeanProperty("skills")
    public void setSkills(List<Skill> skills) {
        this.skills = skills;
    }

    public Member withSkills(List<Skill> skills) {
        this.skills = skills;
        return this;
    }

    /**
     * 
     * @return
     *     The profiles
     */
    @JsonProperty("profiles")
    @BeanProperty("profiles")
    public Profiles getProfiles() {
        return profiles;
    }

    /**
     * 
     * @param profiles
     *     The profiles
     */
    @JsonProperty("profiles")
    @BeanProperty("profiles")
    public void setProfiles(Profiles profiles) {
        this.profiles = profiles;
    }

    public Member withProfiles(Profiles profiles) {
        this.profiles = profiles;
        return this;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(username).append(email).append(picture).append(adr).append(bday).append(tel).append(title).append(url).append(skills).append(profiles).toHashCode();
    }

    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if ((other instanceof Member) == false) {
            return false;
        }
        Member rhs = ((Member) other);
        return new EqualsBuilder().append(username, rhs.username).append(email, rhs.email).append(picture, rhs.picture).append(adr, rhs.adr).append(bday, rhs.bday).append(tel, rhs.tel).append(title, rhs.title).append(url, rhs.url).append(skills, rhs.skills).append(profiles, rhs.profiles).isEquals();
    }

}

 

Great.  Now any resource which returns a MemberHtml or a List<MemberHtml> to the Juneau HTML Serializer will see the freemarker template applied to any Adr instances within the appropriate cell.

BEFORE:

AFTER:

Test blog post #2

The @RestMethod.bpIncludes()/bpExcludes() annotations have been replaced simplified syntax.

Instead of LAX JSON, it's now an array of simple key/value pairs and the names have been shortened:

// Old:
@RestMethod(
   bpExcludes="{Bean1: 'foo,bar', '*': 'baz,qux'}"
)
// New:
@RestMethod(
   bpx={"Bean1: foo,bar", "*: baz,qux"}
)

See:
http://juneau.incubator.apache.org/site/apidocs/org/apache/juneau/rest/annotation/RestMethod.html#bpi--
http://juneau.incubator.apache.org/site/apidocs/org/apache/juneau/rest/annotation/RestMethod.html#bpx--

I've simplified the way the headers are defined on the HTML pages.

I've eliminated the @HtmlDoc.title()/description()/branding() annotations (and related methods and properties).  Instead, you just use the @HtmlDoc.header() with variables to define the header.

The default header value is defined in RestServletDefault as shown:

You can easily override this value to specify any value you wish.  This makes the header much more flexible and easier-to-understand.

The $R variable was changed to a DefaultingVar so that you can chain possible values to find a first-match (as shown above).

I've also added a new $F variable for pulling in values directly from localized files if you don't want to embed a bunch of HTML inside your annotations:

It's similar to widgets except it resolves to just arbitrary text (and doesn't support javascript and css like widgets do).
It's also similar to Class.getResourceAsStream(), but searches up the servlet class hierarchy for the file.  It can even pull the file from the JVM working directory as last resort.  
It also supports localized names based on the request Accept-Language.  So in the example above, if the request language is Japanese, we'll also search for DockerRegistryResourceAside_ja_JP.html and DockerRegistryResourceAside_ja.html.

Like widgets, it also strips comments from HTML files so you can add Apache license headers to your files.

Javadoc is here:  http://juneau.incubator.apache.org/site/apidocs/org/apache/juneau/rest/vars/FileVar.html

Just like widgets, the $F variable file contents can contain other variables.  (Heck...it can even contain other $F variables if you want to do fancy recursive nesting of files)

The full list of variables has been update here and linked to from various locations where they can be used:
http://juneau.incubator.apache.org/site/apidocs/org/apache/juneau/rest/RestContext.html#getVarResolver--

 

I've added an "INHERIT" literal string that can be used in the @HtmlDoc annotation to inherit and extend values from the servlet class.

Where this is useful is when you want to define a a set of links on a class, but want to add an additional link on a particular method.  Previously this meant you had to duplicate all the links on the method annotation such as shown below from the PetStore example:

Links defined on class:

Links overridden on method to add a single QUERY menu item:

With the "INHERIT" literal, you can include the parent links and augment them with more links like so:

The parent links are included in the position specified in the array, so you can include links before or after the list parent links, or you can optionally use square brackets to specify an index position if you want to insert your link in the middle of the parent links.  

The "INHERIT" literal can be used in any of the following @HtmlDoc values:  header, script, style, links, aside, footer.

When used on the @HtmlDoc in the @RestResource annotation, it inherits the value from the parent class.

 

I've made several overall improvements to the stylesheet support in the HTML views.

  1. I've simplified the CSS stylesheets.  It should be easier to create new stylesheets.

  2. The nav links are now a <OL> instead of a span of hardcoded links.  Makes it easier to customize the LAF of the nav section through CSS.

  3. New "light" stylesheet...

    For reference, this is the "devops" style...

    And this is the "original" style...


  4. I've added a menu-time to the examples that allow you to view the different styles...

 

FYI....I've made the following modifications to the following @HtmlDoc annotations:

  • links() - Now an array of strings instead of a JSON object. Simplified syntax. 
    For example:

    Previous syntax will still work, but you're encouraged to use the simplified syntax.
  • Several annotations are now arrays of strings instead of simple strings. Values are simply concatenated with newlines which makes multi-line values cleaner.