
io.neba.core.rendering.BeanRendererImpl Maven / Gradle / Ivy
/**
* Copyright 2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.neba.core.rendering;
import io.neba.api.Constants;
import io.neba.api.rendering.BeanRenderer;
import io.neba.core.util.FastStringWriter;
import org.apache.sling.scripting.api.BindingsValuesProvider;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import java.io.Writer;
import java.util.List;
import java.util.Map;
import static io.neba.core.util.ClassHierarchyIterator.hierarchyOf;
import static org.apache.commons.lang.StringUtils.endsWith;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.springframework.util.Assert.notNull;
/**
* Renders an object using the provided view repository path, the object's
* {@link io.neba.core.util.ClassHierarchyIterator class hierarchy} and a provided viewHint to
* build possible template names for the object. Supports adding additional elements to the
* rendering context.
*
*
* Example: For a class io.neba.MyModel implementing the interface
* io.neba.MyInterface, the renderer will attempt to resolve the following
* templates (in this particular order according to the
* {@link io.neba.core.util.ClassHierarchyIterator}), if the repositoryPath is "/apps/views/" and
* the view hint is null:
*
*
*
* - /apps/views/io/neba/MyModel.vlt
* - /apps/views/io/neba/MyInterface.vlt
* - /java/lang/Object.vlt
*
*
*
* The first view found is used to render the object.
*
*
*
* Likewise, when rendering with a viewhint, e.g. "teaser", the following views
* would be resolved:
*
*
*
* - /apps/views/io/neba/MyModel-teaser.vlt
* - /apps/views/io/neba/MyInterface-teaser.vlt
* - /java/lang/Object-teaser.vlt
*
*
* TODO: Performance optimization: Caching of failed lookups and hierarchy iteration.
*
* @author Olaf Otto
*/
public class BeanRendererImpl implements BeanRenderer {
private final String repositoryPath;
private final VelocityEngine engine;
private final List bindingsValuesProviders;
public BeanRendererImpl(String repositoryPath, VelocityEngine engine, List bindingsValuesProviders) {
notNull(repositoryPath, "The view repository root path must not be null.");
notNull(engine, "The velocity engine must not be null.");
this.repositoryPath = normalize(repositoryPath);
this.engine = engine;
this.bindingsValuesProviders = bindingsValuesProviders;
}
private String normalize(String repositoryPath) {
if (!endsWith(repositoryPath, "/")) {
return repositoryPath + "/";
}
return repositoryPath;
}
@Override
public String render(Object bean, String viewHint, Map additionalContextElements) {
notNull(bean, "Method argument bean must not be null.");
String renderedObject = null;
for (Class> type : hierarchyOf(bean.getClass())) {
String templatePath = createTemplateName(viewHint, type);
if (this.engine.resourceExists(templatePath)) {
renderedObject = renderInternal(bean, templatePath, additionalContextElements);
break;
}
}
return renderedObject;
}
public String renderInternal(Object object, String templatePath, Map additionalContextElements) {
final Writer writer = new FastStringWriter();
final VelocityBindings bindings = prepareBindings(object);
merge(additionalContextElements, bindings);
final VelocityContext context = new VelocityContext(bindings);
String renderedObject;
try {
this.engine.getTemplate(templatePath).merge(context, writer);
} catch (Exception e) {
throw new RuntimeException("Unable to render " + object + " with template " + templatePath + ".", e);
}
renderedObject = writer.toString();
return renderedObject;
}
private VelocityBindings prepareBindings(Object model) {
VelocityBindings bindings = new VelocityBindings();
for (BindingsValuesProvider provider : this.bindingsValuesProviders) {
provider.addBindings(bindings);
}
bindings.put(Constants.RENDERER, this);
bindings.put(Constants.MODEL, model);
return bindings;
}
public String createTemplateName(String viewHint, Class> type) {
StringBuilder templatePathBuilder = new StringBuilder(128);
templatePathBuilder.append(this.repositoryPath).append(type.getName().replace('.', '/'));
if (!isBlank(viewHint)) {
templatePathBuilder.append('-').append(viewHint);
}
templatePathBuilder.append(".vlt");
return templatePathBuilder.toString();
}
private void merge(Map context, final VelocityBindings bindings) {
if (context != null) {
for (Map.Entry entry : context.entrySet()) {
final String key = entry.getKey();
if (bindings.containsKey(key)) {
throw new IllegalArgumentException("The bindings already contain the key '" + key + "' (a " + bindings.get(key) + ").");
} else {
bindings.put(key, entry.getValue());
}
}
}
}
@Override
public String render(Object bean) {
return render(bean, null, null);
}
@Override
public String render(Object bean, Map context) {
return render(bean, null, context);
}
@Override
public String render(Object bean, String viewHint) {
return render(bean, viewHint, null);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy