In the age of AI, not only does everything happen faster, but you also always have a user by your side who shows you what isn’t quite as it should be yet. An experiment with AI led to a new little feature. But first things first.
A long-cherished dream for FreshMarker was brought to life as an experiment with the help of AI. The result is a Spring Boot Starter for FreshMarker. But today’s post isn’t about this Spring Boot Starter just yet. It will be introduced soon, though.
To demonstrate the implementation possibilities, a sample MVC application was also added. The AI made an effort to leverage the capabilities of FreshMarker. This included modeling model classes as records.
public record Product(
int id,
String name,
String category,
String description,
MonetaryAmount price,
int stock
) {
public boolean available() {
return stock > 0;
}
}
A product with various attributes is represented here as a record. In addition, the record has a method called available that checks whether the product is in stock. This record should be used in the following template.
<div class="product-card__footer">
<strong class="product-card__price">${p.price}</strong>
<#if p.available>
<span class="stock stock--ok">${p.stock} in stock</span>
<#else>
<span class="stock stock--empty">Unavailable</span>
</#if>
</div>
The If-Directive checks the value of p.available and then outputs one of the two options. The usage is intuitively correct, but unfortunately the template does not work. The expression p.available does not return a boolean, but rather the value NULL. For the user—and in this case, the AI—it naturally doesn’t make sense why p.stock() works in the template as ${p.stock}, but p.available() does not work in the template as ${p.available}. The reason for this is the implementation of the RecordMethodProvider.
public class RecordMethodProvider implements Function<Class<?>, Map<String, Method>> {
@Override
public Map<String, Method> apply(Class<?> type) {
return Stream.of(type.getRecordComponents())
.collect(Collectors.toMap(RecordComponent::getName, RecordComponent::getAccessor));
}
}
This class creates a map from the record’s RecordComponents. This map contains all the attributes that are accessible in the template. However, the public method available is not a RecordComponent. Therefore, accessing the map with the key “available” returns the value null. How can we add the available method to the map?
public class RecordMethodProvider implements Function<Class<?>, Map<String, Method>> {
private static final Set<String> OBJECT_METHOD_NAMES = getObjectMethodNames();
@Override
public Map<String, Method> apply(Class<?> type) {
Map<String, Method> result = new HashMap<>();
for (Method method : type.getMethods()) {
if (method.getParameterCount() == 0 && !Modifier.isStatic(method.getModifiers())
&& !OBJECT_METHOD_NAMES.contains(method.getName())) {
result.put(method.getName(), method);
}
}
return result;
}
private static Set<String> getObjectMethodNames() {
Set<String> objectMethodNames = new HashSet<>();
for (Method method : Object.class.getMethods()) {
if (method.getParameterCount() == 0 && !Modifier.isStatic(method.getModifiers())) {
objectMethodNames.add(method.getName());
}
}
return objectMethodNames;
}
}
At this point, we use classic Reflections: we iterate through all methods and collect all parameterless public instance methods. To avoid collecting methods like toString, we ignore all corresponding methods from the base class Object.
This completes the implementation of the new feature, and KI and other users can now also use public, parameterless methods of Records. The question that arises in the end is: Why couldn’t the Record implementation do this from the start? Presumably because the feature was implemented based on technical requirements at the time and was not driven by user requirements.
This feature will be included in the next version of FreshMarker. Please be patient.