blob: 23c0cb8a4a2f5b6aa2d3dd5481ae12aefdbfe85d [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you 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 org.apache.sling.scripting.sightly.render;
import java.io.PrintWriter;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.script.Bindings;
import javax.script.SimpleBindings;
import org.apache.sling.scripting.sightly.Record;
/**
* Basic unit of rendering. This also extends the record interface. The properties for a unit are the sub-units.
*/
public abstract class RenderUnit implements Record<RenderUnit> {
private final Map<String, RenderUnit> subTemplates = new HashMap<>();
private Map<String, RenderUnit> siblings;
/**
* Render the main script template
*
* @param out the {@link PrintWriter} to which the commands are written
* @param renderContext the rendering context
* @param arguments the arguments for this unit
*/
public final void render(PrintWriter out, RenderContext renderContext, Bindings arguments) {
Bindings globalBindings = renderContext.getBindings();
render(out, buildGlobalScope(globalBindings), new CaseInsensitiveBindings(arguments), renderContext);
}
@Override
public RenderUnit getProperty(String name) {
return subTemplates.get(name.toLowerCase());
}
@Override
public Set<String> getPropertyNames() {
return subTemplates.keySet();
}
protected abstract void render(PrintWriter out,
Bindings bindings,
Bindings arguments,
RenderContext renderContext);
@SuppressWarnings({"unused", "unchecked"})
protected void callUnit(PrintWriter out, RenderContext renderContext, Object templateObj, Object argsObj) {
if (!(templateObj instanceof RenderUnit)) {
if (templateObj == null) {
throw new RuntimeException("data-sly-call: expression evaluates to null.");
}
if (renderContext.getObjectModel().isPrimitive(templateObj)) {
throw new RuntimeException(
"data-sly-call: primitive \"" + templateObj.toString() + "\" does not represent a HTL template.");
} else if (templateObj instanceof String) {
throw new RuntimeException(
"data-sly-call: String '" + templateObj.toString() + "' does not represent a HTL template.");
}
throw new RuntimeException(
"data-sly-call: " + templateObj.getClass().getName() + " does not represent a HTL template.");
}
RenderUnit unit = (RenderUnit) templateObj;
Map<String, Object> argumentsMap = renderContext.getObjectModel().toMap(argsObj);
Bindings arguments = new SimpleBindings(Collections.unmodifiableMap(argumentsMap));
unit.render(out, renderContext, arguments);
}
@SuppressWarnings("UnusedDeclaration")
protected FluentMap obj() {
return new FluentMap();
}
@SuppressWarnings("unused")
protected final void addSubTemplate(String name, RenderUnit renderUnit) {
renderUnit.setSiblings(subTemplates);
subTemplates.put(name.toLowerCase(), renderUnit);
}
private void setSiblings(Map<String, RenderUnit> siblings) {
this.siblings = siblings;
}
private Bindings buildGlobalScope(Bindings bindings) {
CaseInsensitiveBindings caseInsensitiveBindings = new CaseInsensitiveBindings(bindings);
if (siblings != null) {
caseInsensitiveBindings.putAll(siblings);
}
caseInsensitiveBindings.putAll(subTemplates);
return caseInsensitiveBindings;
}
protected static class FluentMap extends HashMap<String, Object> {
/**
* Fluent variant of put.
*
* @param name the name of the property
* @param value the value of the property
* @return this instance
*/
public FluentMap with(String name, Object value) {
put(name, value);
return this;
}
}
private static final class CaseInsensitiveBindings implements Bindings {
private final Map<String, Object> wrapped;
private final Map<String, String> keyMappings;
private CaseInsensitiveBindings(Map<String, Object> wrapped) {
this.wrapped = wrapped;
keyMappings = new HashMap<>();
for (String key : this.wrapped.keySet()) {
keyMappings.put(key.toLowerCase(), key);
}
}
@Override
public Object get(Object key) {
if (!(key instanceof String)) {
throw new ClassCastException("key should be a String");
}
String mappedKey = keyMappings.get(((String) key).toLowerCase());
if (mappedKey != null) {
return wrapped.get(mappedKey);
}
return null;
}
@Override
public boolean containsKey(Object key) {
if (!(key instanceof String)) {
throw new ClassCastException("key should be a String");
}
return keyMappings.containsKey(((String) key).toLowerCase());
}
@Override
public Object put(String key, Object value) {
keyMappings.put(key.toLowerCase(), key);
return wrapped.put(key, value);
}
@Override
public void putAll(Map<? extends String, ?> toMerge) {
for (Map.Entry<? extends String, ?> entry : toMerge.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public Object remove(Object key) {
if (!(key instanceof String)) {
throw new ClassCastException("key should be a String");
}
String originalKey = keyMappings.remove(((String) key).toLowerCase());
if (originalKey != null) {
return wrapped.remove(originalKey);
}
return null;
}
@Override
public int size() {
return wrapped.size();
}
@Override
public boolean isEmpty() {
return wrapped.isEmpty();
}
@Override
public boolean containsValue(Object value) {
return wrapped.containsValue(value);
}
@Override
public void clear() {
wrapped.clear();
keyMappings.clear();
}
@Override
public Set<String> keySet() {
return keyMappings.keySet();
}
@Override
public Collection<Object> values() {
return wrapped.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
Set<Entry<String, Object>> entrySet = new HashSet<>();
for (Map.Entry<String, String> entry : keyMappings.entrySet()) {
entrySet.add(new AbstractMap.SimpleEntry<>(entry.getKey(), wrapped.get(entry.getValue())));
}
return entrySet;
}
}
}