Skip to content

Commit

Permalink
Qute - improve Java array support
Browse files Browse the repository at this point in the history
- make it possible to get the length of the specified array and access
the elements directly via an index value
- support array-based template parameter declarations
  • Loading branch information
mkouba committed Feb 4, 2021
1 parent 5ef0975 commit 2b01ca7
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 64 deletions.
25 changes: 25 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,30 @@ TIP: The condition in a ternary operator evaluates to `true` if the value is not

NOTE: In fact, the operators are implemented as "virtual methods" that consume one parameter and can be used with infix notation, i.e. `{person.name or 'John'}` is translated to `{person.name.or('John')}`.

==== Arrays

You can iterate over elements of an array with the <<loop_section>>. Moreover, it's also possible to get the length of the specified array and access the elements directly via an index value.

.Array Examples
[source,html]
----
<h1>Array of length: {myArray.length}</h1> <1>
<ul>
<li>First: {myArray.0}</li> <2>
<li>Second: {myArray[1]}</li> <3>
<li>Third: {myArray.get(2)}</li> <4>
</ul>
<ol>
{#for element in myArray}
<li>{element}</li>
{/for}
</ol>
----
<1> Outputs the length of the array.
<2> Outputs the first element of the array.
<3> Outputs the second element of the array using the bracket notation.
<4> Outputs the third element of the array via the virtual method `get()`.

==== Character Escapes

For HTML and XML templates the `'`, `"`, `<`, `>`, `&` characters are escaped by default if a template variant is set.
Expand Down Expand Up @@ -482,6 +506,7 @@ A section helper that defines the logic of a section can "execute" any of the bl
{/if}
----

[[loop_section]]
==== Loop Section

The loop section makes it possible to iterate over an instance of `Iterable`, `Map` 's entry set, `Stream` and an Integer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
Expand All @@ -45,6 +46,7 @@
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.PrimitiveType;
import org.jboss.jandex.Type;
import org.jboss.jandex.TypeVariable;
import org.jboss.logging.Logger;
Expand Down Expand Up @@ -572,55 +574,100 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
}

while (iterator.hasNext()) {
// Now iterate over all parts of the expression and check each part against the current "match class"
// Now iterate over all parts of the expression and check each part against the current match type
Info info = iterator.next();
if (!match.isEmpty()) {
// By default, we only consider properties
Set<String> membersUsed = implicitClassToMembersUsed.get(match.clazz().name());
if (membersUsed == null) {
membersUsed = new HashSet<>();
implicitClassToMembersUsed.put(match.clazz().name(), membersUsed);

// Arrays are handled specifically
// We use the built-in resolver at runtime because the extension methods cannot be used to cover all combinations of dimensions and component types
if (match.isArray()) {
if (info.isProperty()) {
String name = info.asProperty().name;
if (name.equals("length")) {
// myArray.length
match.setValues(null, PrimitiveType.INT);
continue;
} else {
// myArray[0], myArray.1
try {
Integer.parseInt(name);
match.setValues(null, match.type().asArrayType().component());
continue;
} catch (NumberFormatException e) {
// not an integer index
}
}
} else if (info.isVirtualMethod() && info.asVirtualMethod().name.equals("get")) {
// array.get(84)
List<Expression> params = info.asVirtualMethod().part.asVirtualMethod().getParameters();
if (params.size() == 1) {
Expression param = params.get(0);
Object literalValue;
try {
literalValue = param.getLiteralValue().get();
} catch (InterruptedException | ExecutionException e) {
literalValue = null;
}
if (literalValue == null || literalValue instanceof Integer) {
match.setValues(null, match.type().asArrayType().component());
continue;
}
}
}
}

AnnotationTarget member = null;
// First try to find a java member
if (info.isVirtualMethod()) {
member = findMethod(info.part.asVirtualMethod(), match.clazz(), expression, index, templateIdToPathFun,
results);
if (member != null) {
membersUsed.add(member.asMethod().name());

if (!match.isPrimitive()) {
Set<String> membersUsed = implicitClassToMembersUsed.get(match.type().name());
if (membersUsed == null) {
membersUsed = new HashSet<>();
implicitClassToMembersUsed.put(match.type().name(), membersUsed);
}
// First try to find a java member
if (match.clazz() != null) {
if (info.isVirtualMethod()) {
member = findMethod(info.part.asVirtualMethod(), match.clazz(), expression, index,
templateIdToPathFun,
results);
if (member != null) {
membersUsed.add(member.asMethod().name());
}
} else if (info.isProperty()) {
member = findProperty(info.asProperty().name, match.clazz(), index);
if (member != null) {
membersUsed
.add(member.kind() == Kind.FIELD ? member.asField().name() : member.asMethod().name());
}
}
}
} else if (info.isProperty()) {
member = findProperty(info.asProperty().name, match.clazz(), index);
if (member != null) {
membersUsed.add(member.kind() == Kind.FIELD ? member.asField().name() : member.asMethod().name());
if (member == null) {
// Then try to find an etension method
member = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression,
index,
templateIdToPathFun, results);
}
}
if (member == null) {
// Then try to find an etension method
member = findTemplateExtensionMethod(info, match.clazz(), templateExtensionMethods, expression,
index,
templateIdToPathFun, results);
}

if (member == null) {
// Test whether the validation should be skipped
TypeCheck check = new TypeCheck(info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name,
match.clazz(),
info.part.isVirtualMethod() ? info.part.asVirtualMethod().getParameters().size() : -1);
if (isExcluded(check, excludes)) {
LOGGER.debugf(
"Expression part [%s] excluded from validation of [%s] against class [%s]",
info.value,
expression.toOriginalString(), match.clazz());
match.clearValues();
break;
if (member == null) {
// Test whether the validation should be skipped
TypeCheck check = new TypeCheck(
info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name,
match.clazz(),
info.part.isVirtualMethod() ? info.part.asVirtualMethod().getParameters().size() : -1);
if (isExcluded(check, excludes)) {
LOGGER.debugf(
"Expression part [%s] excluded from validation of [%s] against type [%s]",
info.value,
expression.toOriginalString(), match.type());
match.clearValues();
break;
}
}
}

if (member == null) {
// No member found - incorrect expression
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
info.value, match.clazz().toString(), expression.getOrigin()));
info.value, match.type().toString(), expression.getOrigin()));
match.clearValues();
break;
} else {
Expand All @@ -638,9 +685,6 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
incorrectExpressions);
}
}
if (match.isPrimitive()) {
break;
}
}
} else {
LOGGER.debugf(
Expand Down Expand Up @@ -736,8 +780,7 @@ private void produceExtensionMethod(IndexView index, BuildProducer<TemplateExten
matchRegex = matchRegexValue.asString();
}
extensionMethods.produce(new TemplateExtensionMethodBuildItem(method, matchName, matchRegex,
namespace.isEmpty() ? index.getClassByName(method.parameters().get(0).name()) : null,
priority, namespace));
method.parameters().get(0), priority, namespace));
}

private void validateInjectExpression(TemplateAnalysis templateAnalysis, Expression expression, IndexView index,
Expand Down Expand Up @@ -1352,7 +1395,8 @@ void clearValues() {
}

boolean isEmpty() {
return clazz == null;
// For arrays the class is null
return type == null;
}

void autoExtractType() {
Expand All @@ -1371,7 +1415,7 @@ void autoExtractType() {
}
}

private static AnnotationTarget findTemplateExtensionMethod(Info info, ClassInfo matchClass,
private static AnnotationTarget findTemplateExtensionMethod(Info info, Type matchType,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods, Expression expression, IndexView index,
Function<String, String> templateIdToPathFun, Map<String, Match> results) {
if (!info.isProperty() && !info.isVirtualMethod()) {
Expand All @@ -1383,7 +1427,7 @@ private static AnnotationTarget findTemplateExtensionMethod(Info info, ClassInfo
// Skip namespace extensions
continue;
}
if (!Types.isAssignableFrom(extensionMethod.getMatchClass().name(), matchClass.name(), index)) {
if (!Types.isAssignableFrom(extensionMethod.getMatchType(), matchType, index)) {
// If "Bar extends Foo" then Bar should be matched for the extension method "int get(Foo)"
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.qute.TemplateExtension;
Expand All @@ -19,17 +20,16 @@ public final class TemplateExtensionMethodBuildItem extends MultiBuildItem {
private final String matchName;
private final String matchRegex;
private final Pattern matchPattern;
private final ClassInfo matchClass;
private final Type matchType;
private final int priority;
private final String namespace;

public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, String matchRegex, ClassInfo matchClass,
int priority,
String namespace) {
public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, String matchRegex, Type matchType,
int priority, String namespace) {
this.method = method;
this.matchName = matchName;
this.matchRegex = matchRegex;
this.matchClass = matchClass;
this.matchType = matchType;
this.priority = priority;
this.namespace = namespace;
this.matchPattern = (matchRegex == null || matchRegex.isEmpty()) ? null : Pattern.compile(matchRegex);
Expand All @@ -47,8 +47,8 @@ public String getMatchRegex() {
return matchRegex;
}

public ClassInfo getMatchClass() {
return matchClass;
public Type getMatchType() {
return matchType;
}

public int getPriority() {
Expand All @@ -60,7 +60,7 @@ public String getNamespace() {
}

boolean matchesClass(ClassInfo clazz) {
return matchClass.name().equals(clazz.name());
return matchType.name().equals(clazz.name());
}

boolean matchesName(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import java.util.List;
import java.util.function.Function;

import org.jboss.jandex.ArrayType;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.PrimitiveType;
import org.jboss.jandex.Type;
import org.jboss.jandex.Type.Kind;

Expand All @@ -19,6 +21,8 @@

final class TypeInfos {

private static final String ARRAY_DIM = "[]";

static List<Info> create(Expression expression, IndexView index, Function<String, String> templateIdToPathFun) {
if (expression.isLiteral()) {
Expression.Part literalPart = expression.getParts().get(0);
Expand Down Expand Up @@ -53,16 +57,24 @@ static Info create(String typeInfo, Expression.Part part, IndexView index, Funct
if (classStr.equals(Expressions.TYPECHECK_NAMESPACE_PLACEHOLDER)) {
return new Info(typeInfo, part);
} else {
DotName rawClassName = rawClassName(classStr);
ClassInfo rawClass = index.getClassByName(rawClassName);
if (rawClass == null) {
throw new TemplateException(
"Class [" + rawClassName + "] used in the parameter declaration in template ["
+ templateIdToPathFun.apply(expressionOrigin.getTemplateGeneratedId()) + "] on line "
+ expressionOrigin.getLine()
+ " was not found in the application index. Make sure it is spelled correctly.");
// TODO make the parsing logic more robust
ClassInfo rawClass;
Type resolvedType;
int idx = classStr.indexOf(ARRAY_DIM);
if (idx > 0) {
// int[], java.lang.String[][], etc.
String componentTypeStr = classStr.substring(0, idx);
Type componentType = decodePrimitive(componentTypeStr);
if (componentType == null) {
componentType = resolveType(componentTypeStr);
}
String[] dimensions = classStr.substring(idx, classStr.length()).split("\\]");
rawClass = null;
resolvedType = ArrayType.create(componentType, dimensions.length);
} else {
rawClass = getClassInfo(classStr, index, templateIdToPathFun, expressionOrigin);
resolvedType = resolveType(classStr);
}
Type resolvedType = resolveType(classStr);
return new TypeInfo(typeInfo, part, helperHint(typeInfo.substring(endIdx, typeInfo.length())), resolvedType,
rawClass);
}
Expand All @@ -78,6 +90,43 @@ static Info create(String typeInfo, Expression.Part part, IndexView index, Funct
}
}

private static ClassInfo getClassInfo(String val, IndexView index, Function<String, String> templateIdToPathFun,
Origin expressionOrigin) {
DotName rawClassName = rawClassName(val);
ClassInfo clazz = index.getClassByName(rawClassName);
if (clazz == null) {
throw new TemplateException(
"Class [" + rawClassName + "] used in the parameter declaration in template ["
+ templateIdToPathFun.apply(expressionOrigin.getTemplateGeneratedId()) + "] on line "
+ expressionOrigin.getLine()
+ " was not found in the application index. Make sure it is spelled correctly.");
}
return clazz;
}

private static PrimitiveType decodePrimitive(String val) {
switch (val) {
case "byte":
return PrimitiveType.BYTE;
case "char":
return PrimitiveType.CHAR;
case "double":
return PrimitiveType.DOUBLE;
case "float":
return PrimitiveType.FLOAT;
case "int":
return PrimitiveType.INT;
case "long":
return PrimitiveType.LONG;
case "short":
return PrimitiveType.SHORT;
case "boolean":
return PrimitiveType.BOOLEAN;
default:
return null;
}
}

static final String LEFT_ANGLE = "<";
static final String RIGHT_ANGLE = ">";
static final String TYPE_INFO_SEPARATOR = "" + Expressions.TYPE_INFO_SEPARATOR;
Expand Down
Loading

0 comments on commit 2b01ca7

Please sign in to comment.