8 Commits

Author SHA1 Message Date
David
92aa3310db Add parameter name reflection 2018-01-10 16:06:52 +01:00
David
b42ba41697 Update README.md 2017-12-31 12:25:35 +01:00
David
0c7f75f5b8 Update README.md 2017-12-31 12:25:05 +01:00
Janning Vygen
754003ebf6 Add attr(Attribute attribute) to Tag 2017-12-10 16:50:15 +01:00
David
6968478894 add multiplication-table to perf-test 2017-12-08 19:36:41 +01:00
David
0b92a963e9 add some ignored performance-tests 2017-12-07 22:10:34 +01:00
David
ac92facca7 Update README.md 2017-12-06 18:11:29 +01:00
David
4cf16320ad [maven-release-plugin] prepare for next development iteration 2017-12-06 17:04:20 +01:00
21 changed files with 516 additions and 15 deletions

View File

@@ -13,12 +13,12 @@ The project webpage is [j2html.com](http://j2html.com).
<dependency>
<groupId>com.j2html</groupId>
<artifactId>j2html</artifactId>
<version>1.2.1</version>
<version>1.2.2</version>
</dependency>
```
### OR the gradle dependency
### Or the gradle dependency
```
compile 'com.j2html:j2html:1.2.1'
compile 'com.j2html:j2html:1.2.2'
```
### Import TagCreator and start building HTML
@@ -27,9 +27,9 @@ import static j2html.TagCreator.*;
public class Main {
public static void main(String[] args) {
body().with(
h1("Heading!").withClass("example"),
img().withSrc("img/hello.png")
body(
h1("Hello, World!"),
img().withSrc("/img/hello.png")
).render();
}
}
@@ -37,8 +37,8 @@ public class Main {
The above Java will result in the following HTML:
```html
<body>
<h1 class="example">Heading!</h1>
<img src="img/hello.png">
<h1>Hello, World!</h1>
<img src="/img/hello.png">
</body>
```

22
pom.xml
View File

@@ -10,7 +10,7 @@
<groupId>com.j2html</groupId>
<artifactId>j2html</artifactId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<name>j2html</name>
<description>Java to HTML builder with a fluent API</description>
@@ -63,7 +63,22 @@
<artifactId>junit-benchmarks</artifactId>
<version>0.7.2</version>
<scope>test</scope>
</dependency>
</dependency>
<!-- performance test dependencies -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>test</scope>
</dependency>
</dependencies>
<packaging>jar</packaging>
@@ -78,6 +93,9 @@
<source>1.8</source>
<target>1.8</target>
<optimize>true</optimize>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>

View File

@@ -0,0 +1,24 @@
package j2html.attributes;
import j2html.reflection.MethodFinder;
import java.util.function.Function;
public interface LambdaAttribute extends MethodFinder, Function<String, Object> {
default String name() {
checkParametersEnabled();
return parameter(0).getName();
}
default String value() {
checkParametersEnabled();
return String.valueOf(this.apply(name()));
}
default void checkParametersEnabled() {
if ("arg0".equals(parameter(0).getName())) {
throw new IllegalStateException("You need java 8u60 or newer for parameter reflection to work");
}
}
}

View File

@@ -0,0 +1,48 @@
package j2html.reflection;
// Written by Benjamin Weber (http://benjiweber.co.uk/blog/author/benji/)
import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Objects;
public interface MethodFinder extends Serializable {
default SerializedLambda serialized() {
try {
Method replaceMethod = getClass().getDeclaredMethod("writeReplace");
replaceMethod.setAccessible(true);
return (SerializedLambda) replaceMethod.invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
default Class<?> getContainingClass() {
try {
String className = serialized().getImplClass().replaceAll("/", ".");
return Class.forName(className);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
default Method method() {
SerializedLambda lambda = serialized();
Class<?> containingClass = getContainingClass();
return Arrays.stream(containingClass.getDeclaredMethods())
.filter(method -> Objects.equals(method.getName(), lambda.getImplMethodName()))
.findFirst()
.orElseThrow(UnableToGuessMethodException::new);
}
default Parameter parameter(int n) {
return method().getParameters()[n];
}
class UnableToGuessMethodException extends RuntimeException {
}
}

View File

@@ -2,8 +2,10 @@ package j2html.tags;
import j2html.attributes.Attr;
import j2html.attributes.Attribute;
import j2html.attributes.LambdaAttribute;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
public abstract class Tag<T extends Tag<T>> extends DomContent {
protected String tagName;
@@ -79,6 +81,28 @@ public abstract class Tag<T extends Tag<T>> extends DomContent {
return (T) this;
}
/**
* Adds the specified attribute. If the Tag previously contained an attribute with the same name, the old attribute is replaced by the specified attribute.
*
* @param attribute the attribute
* @return itself for easy chaining
*/
public T attr(Attribute attribute) {
Iterator<Attribute> iterator = attributes.iterator();
String name = attribute.getName();
if (name != null) {
// name == null is allowed, but those Attributes are not rendered. So we add them anyway.
while (iterator.hasNext()) {
Attribute existingAttribute = iterator.next();
if (existingAttribute.getName().equals(name)) {
iterator.remove();
}
}
}
attributes.add(attribute);
return (T) this;
}
/**
* Sets a custom attribute without value
*
@@ -111,11 +135,6 @@ public abstract class Tag<T extends Tag<T>> extends DomContent {
return ((Tag) obj).render().equals(this.render());
}
/**
* Convenience methods that call attr with predefined attributes
*
* @return itself for easy chaining
*/
public T withClasses(String... classes) {
StringBuilder sb = new StringBuilder();
for (String s : classes) {
@@ -124,6 +143,19 @@ public abstract class Tag<T extends Tag<T>> extends DomContent {
return attr(Attr.CLASS, sb.toString().trim());
}
public T withAttrs(LambdaAttribute... lambdaAttributes) {
for (LambdaAttribute attr : lambdaAttributes) {
attr(attr.name(), attr.value());
}
return (T) this;
}
/**
* Convenience methods that call attr with predefined attributes
*
* @return itself for easy chaining
*/
public T isAutoComplete() {
return attr(Attr.AUTOCOMPLETE, null);
}

View File

@@ -0,0 +1,17 @@
package j2html.comparison;
import j2html.comparison.model.Employee;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ComparisonData {
public static List<Employee> fiveHundredEmployees() {
return IntStream.range(0, 500).mapToObj(i -> new Employee(i, "Some name", "Some title")).collect(Collectors.toList());
}
public static List<Integer> tableNumbers = IntStream.range(1, 51).boxed().collect(Collectors.toList());
}

View File

@@ -0,0 +1,34 @@
package j2html.comparison;
import com.carrotsearch.junitbenchmarks.BenchmarkOptions;
import com.carrotsearch.junitbenchmarks.BenchmarkRule;
import com.carrotsearch.junitbenchmarks.Clock;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@Ignore
@BenchmarkOptions(callgc = false, benchmarkRounds = 20_000, warmupRounds = 200, concurrency = 2, clock = Clock.NANO_TIME)
public class RenderPerformanceComparisonTest {
@Rule
public TestRule benchmarkRun = new BenchmarkRule();
@Test
public void j2htmlPerformance() throws Exception {
TestJ2html.helloWorld();
TestJ2html.fiveHundredEmployees();
TestJ2html.macros();
TestJ2html.multiplicationTable();
}
@Test
public void velocityPerformance() throws Exception {
TestVelocity.helloWorld();
TestVelocity.fiveHundredEmployees();
TestVelocity.macros();
TestVelocity.multiplicationTable();
}
}

View File

@@ -0,0 +1,30 @@
package j2html.comparison;
import j2html.comparison.j2html.FiveHundredEmployees;
import j2html.comparison.j2html.HelloWorld;
import j2html.comparison.j2html.Macros;
import j2html.comparison.j2html.MultiplicationTable;
public class TestJ2html {
public static String helloWorld() {
return HelloWorld.tag.render();
}
public static String fiveHundredEmployees() {
return FiveHundredEmployees.tag.render();
}
public static String macros() {
return Macros.tag.render();
}
public static String multiplicationTable() {
return MultiplicationTable.tag.render();
}
public static void main(String[] args) {
System.out.println(MultiplicationTable.tag.renderFormatted());
}
}

View File

@@ -0,0 +1,52 @@
package j2html.comparison;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
public class TestVelocity {
private static VelocityEngine velocityEngine;
static {
velocityEngine = new VelocityEngine();
velocityEngine.setProperty("resource.loader", "class");
velocityEngine.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
}
private static String render(String templatePath, Map<String, Object> model) {
StringWriter stringWriter = new StringWriter();
velocityEngine.getTemplate(templatePath, StandardCharsets.UTF_8.name()).merge(
new VelocityContext(model), stringWriter
);
return stringWriter.toString();
}
public static String helloWorld() {
return render("/comparison/velocity/helloWorld.vm", null);
}
public static String fiveHundredEmployees() {
Map<String, Object> model = new HashMap<>();
model.put("employees", ComparisonData.fiveHundredEmployees());
return render("/comparison/velocity/fiveHundredEmployees.vm", model);
}
public static String macros() {
return render("/comparison/velocity/macros.vm", null);
}
public static String multiplicationTable() {
Map<String, Object> model = new HashMap<>();
model.put("tableNumbers", ComparisonData.tableNumbers);
return render("/comparison/velocity/multiplicationTable.vm", model);
}
public static void main(String[] args) {
System.out.println(multiplicationTable());
}
}

View File

@@ -0,0 +1,18 @@
package j2html.comparison.j2html;
import j2html.comparison.ComparisonData;
import j2html.tags.ContainerTag;
import static j2html.TagCreator.each;
import static j2html.TagCreator.li;
import static j2html.TagCreator.ul;
import static org.apache.commons.lang3.StringUtils.join;
public class FiveHundredEmployees {
public static ContainerTag tag = ul(
each(ComparisonData.fiveHundredEmployees(), employee ->
li(join(employee.getId(), employee.getName(), employee.getTitle()))
)
);
}

View File

@@ -0,0 +1,27 @@
package j2html.comparison.j2html;
import j2html.tags.ContainerTag;
import static j2html.TagCreator.attrs;
import static j2html.TagCreator.body;
import static j2html.TagCreator.h1;
import static j2html.TagCreator.head;
import static j2html.TagCreator.html;
import static j2html.TagCreator.link;
import static j2html.TagCreator.main;
import static j2html.TagCreator.title;
public class HelloWorld {
public static ContainerTag tag = html(
head(
title("Title"),
link().withRel("stylesheet").withHref("/css/main.css")
),
body(
main(attrs("#main.content"),
h1("Heading!")
)
)
);
}

View File

@@ -0,0 +1,46 @@
package j2html.comparison.j2html;
import j2html.tags.ContainerTag;
import j2html.tags.DomContent;
import static j2html.TagCreator.attrs;
import static j2html.TagCreator.body;
import static j2html.TagCreator.div;
import static j2html.TagCreator.h1;
import static j2html.TagCreator.head;
import static j2html.TagCreator.html;
import static j2html.TagCreator.link;
import static j2html.TagCreator.main;
import static j2html.TagCreator.title;
public class Macros {
public static ContainerTag tag = mainLayout(
div(
h1("Example content"),
someMacro(1),
someMacro(2),
someMacro(3)
)
);
private static ContainerTag mainLayout(DomContent content) {
return html(
head(
title("Title"),
link().withRel("stylesheet").withHref("/css/main.css")
),
body(
main(attrs("#main.content"),
content
)
)
);
}
private static ContainerTag someMacro(int i) {
return div(
"Macro call " + i
);
}
}

View File

@@ -0,0 +1,19 @@
package j2html.comparison.j2html;
import j2html.comparison.ComparisonData;
import j2html.tags.ContainerTag;
import static j2html.TagCreator.*;
public class MultiplicationTable {
public static ContainerTag tag = table(
tbody(
each(ComparisonData.tableNumbers, i -> tr(
each(ComparisonData.tableNumbers, j -> td(
String.valueOf(i * j)
))
))
)
);
}

View File

@@ -0,0 +1,10 @@
package j2html.comparison.model;
import lombok.Value;
@Value
public class Employee {
int id;
String name;
String title;
}

View File

@@ -0,0 +1,24 @@
package j2html.model;
import j2html.attributes.Attribute;
import java.io.IOException;
public class DynamicHrefAttribute extends Attribute {
public DynamicHrefAttribute() {
super("href");
}
@Override
public void renderModel(Appendable writer, Object model) throws IOException {
writer.append(" ");
writer.append(getName());
writer.append("=\"");
writer.append(getUrl(model));
writer.append("\"");
}
public String getUrl(Object model) {
return "/";
}
}

View File

@@ -0,0 +1,17 @@
package j2html.reflection;
import j2html.attributes.LambdaAttribute;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class NamedValueTest {
@Test
public void testNamedValueWorks() {
LambdaAttribute pair = five -> 5;
assertThat("five", is(pair.name()));
assertThat("5", is(pair.value()));
}
}

View File

@@ -1,10 +1,13 @@
package j2html.tags;
import j2html.Config;
import j2html.model.DynamicHrefAttribute;
import org.junit.Test;
import static j2html.TagCreator.a;
import static j2html.TagCreator.body;
import static j2html.TagCreator.div;
import static j2html.TagCreator.footer;
import static j2html.TagCreator.form;
import static j2html.TagCreator.header;
import static j2html.TagCreator.html;
import static j2html.TagCreator.iff;
@@ -92,5 +95,29 @@ public class TagTest {
assertThat(testTagAttrWithoutAttr.render(), is("<a attribute></a>"));
}
@Test
public void testDynamicAttribute() throws Exception {
ContainerTag testTagWithAttrValueNull = new ContainerTag("a").attr(new DynamicHrefAttribute());
assertThat(testTagWithAttrValueNull.render(), is("<a href=\"/\"></a>"));
}
@Test
public void testDynamicAttributeReplacement() throws Exception {
ContainerTag testTagWithAttrValueNull = new ContainerTag("a").attr("href", "/link").attr(new DynamicHrefAttribute());
assertThat(testTagWithAttrValueNull.render(), is("<a href=\"/\"></a>"));
}
@Test
public void testParameterNameReflectionAttributes() throws Exception {
String expectedAnchor = "<a href=\"http://example.com\">example.com</a>";
String actualAnchor = a("example.com").withAttrs(href -> "http://example.com").render();
assertThat(actualAnchor, is(expectedAnchor));
String expectedForm = "<form method=\"post\" action=\"/form-path\"><input name=\"email\" type=\"email\"><input name=\"password\" type=\"password\"></form>";
String actualForm = form().withAttrs(method -> "post", action -> "/form-path").with(
input().withAttrs(name -> "email", type -> "email"),
input().withAttrs(name -> "password", type -> "password")
).render();
assertThat(actualForm, is(expectedForm));
}
}

View File

@@ -0,0 +1,7 @@
#* @vtlvariable name="employees" type="java.util.List<j2html.comparison.ComparisonData.Employee>" *#
<ul>
#foreach($employee in $employees)
<li>$employee.id $employee.name $employee.title</li>
#end
</ul>

View File

@@ -0,0 +1,11 @@
<html>
<head>
<title>Title</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<main id="main" class="content">
<h1>Heading!</h1>
</main>
</body>
</html>

View File

@@ -0,0 +1,28 @@
#macro(mainLayout)
<html>
<head>
<title>Title</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<main id="main" class="content">
$!bodyContent
</main>
</body>
</html>
#end
#@mainLayout()
<div>
<h1>Example content</h1>
#someMacro(1)
#someMacro(2)
#someMacro(3)
</div>
#end
#macro(someMacro $callNumber)
<div>
Macro call $callNumber
</div>
#end

View File

@@ -0,0 +1,12 @@
<table>
<tbody>
#foreach($i in $tableNumbers)
<tr>
#foreach($j in $tableNumbers)
#set($product = $i * $j)
<td>$product</td>
#end
</tr>
#end
</tbody>
</table>