All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sling.scripting.sightly.render.RenderUnit Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~ 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 {

    private final Map subTemplates = new HashMap<>();

    private Map 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 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 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 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 {

        /**
         * 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 wrapped;
        private final Map keyMappings;

        private CaseInsensitiveBindings(Map 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 toMerge) {
            for (Map.Entry 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 keySet() {
            return keyMappings.keySet();
        }

        @Override
        public Collection values() {
            return wrapped.values();
        }

        @Override
        public Set> entrySet() {
            Set> entrySet = new HashSet<>();
            for (Map.Entry entry : keyMappings.entrySet()) {
                entrySet.add(new AbstractMap.SimpleEntry<>(entry.getKey(), wrapped.get(entry.getValue())));
            }
            return entrySet;
        }
    }

}