/** * This file Copyright (c) 2010-2012 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.templating.freemarker; import freemarker.core.CollectionAndSequence; import freemarker.core.Environment; import freemarker.template.TemplateBooleanModel; import freemarker.template.TemplateCollectionModel; import freemarker.template.TemplateDirectiveBody; import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateScalarModel; import freemarker.template.TemplateSequenceModel; import freemarker.template.utility.DeepUnwrap; import info.magnolia.cms.core.Content; import info.magnolia.freemarker.models.ContentMapModel; import info.magnolia.freemarker.models.ContentModel; import info.magnolia.objectfactory.Components; import info.magnolia.rendering.context.RenderingContext; import info.magnolia.rendering.engine.RenderException; import info.magnolia.rendering.engine.RenderingEngine; import info.magnolia.templating.elements.AbstractContentTemplatingElement; import info.magnolia.templating.elements.TemplatingElement; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import javax.jcr.Node; /** * A base class for freemarker directives used in Magnolia. * * @param <C> the templating element the directive is operating on * @version $Id$ */ public abstract class AbstractDirective<C extends TemplatingElement> implements TemplateDirectiveModel { public static final String PATH_ATTRIBUTE = "path"; public static final String UUID_ATTRIBUTE = "uuid"; public static final String WORKSPACE_ATTRIBUTE = "workspace"; public static final String CONTENT_ATTRIBUTE = "content"; @Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { final C templatingElement = createTemplatingElement(); prepareTemplatingElement(templatingElement, env, params, loopVars, body); // prepareTemplatingElement should have removed the parameters it knows about. if (!params.isEmpty()) { throw new TemplateModelException("Unsupported parameter(s): " + params); } try { templatingElement.begin(env.getOut()); try { doBody(env, body); } finally { templatingElement.end(env.getOut()); } } catch (RenderException e) { throw new TemplateException(e, env); } } protected C createTemplatingElement() { // FIXME use scope instead of fetching the RenderingContext for passing it as an argument final RenderingEngine renderingEngine = Components.getComponent(RenderingEngine.class); final RenderingContext renderingContext = renderingEngine.getRenderingContext(); return Components.getComponentProvider().newInstance(getTemplatingElementClass(), renderingContext); } protected Class<C> getTemplatingElementClass() { // TODO does this support more than one level of subclasses? return (Class<C>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]; } /** * Implementations of this method should prepare the TemplatingElement with the known parameters. * If parameters have been grabbed using the methods provided by this class, they should be removed from * the map, thus leaving an empty map once the method returns. {@link #execute(freemarker.core.Environment, java.util.Map, freemarker.template.TemplateModel[], freemarker.template.TemplateDirectiveBody)} * will throw a TemplateModelException if there are leftover parameters. * <p/> * <strong>note:</strong> The current FreeMarker implementation passes a "new" Map which we can safely manipulate. * is thrown away after the execution of the directive. When no parameters are passed, the Map is readonly, but it * is otherwise a regular HashMap which has been instantiated shortly before the execution of the directive. However, since * this behavior is not mandated by their API, nor documented (at the time of writing, with FreeMarker 2.3.16), we * should exert caution. Unit tests hopefully cover this, so we'll be safe when updating to newer FreeMarker versions. */ protected abstract void prepareTemplatingElement(C templatingElement, Environment env, Map<String, TemplateModel> params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateModelException, IOException; protected void doBody(Environment env, TemplateDirectiveBody body) throws TemplateException, IOException { if (body != null) { body.render(env.getOut()); } } /** * Utility method for directives who need to ensure they're used with or without a body. * If the body is *optional*, this method shouldn't be used. */ protected void checkBody(TemplateDirectiveBody body, boolean needsBody) throws TemplateModelException { if ((body == null) == needsBody) { throw new TemplateModelException("This directive " + (needsBody ? "needs a body" : "does not support a body")); } } protected String mandatoryString(Map<String, TemplateModel> params, String key) throws TemplateModelException { return _param(params, key, TemplateScalarModel.class, true).getAsString(); } protected String string(Map<String, TemplateModel> params, String key, String defaultValue) throws TemplateModelException { final TemplateScalarModel m = _param(params, key, TemplateScalarModel.class, false); if (m == null) { // we've already checked if the param is mandatory already return defaultValue; } return m.getAsString(); } protected boolean mandatoryBool(Map<String, TemplateModel> params, String key) throws TemplateModelException { return _param(params, key, TemplateBooleanModel.class, true).getAsBoolean(); } protected Boolean bool(Map<String, TemplateModel> params, String key, Boolean defaultValue) throws TemplateModelException { final TemplateBooleanModel m = _param(params, key, TemplateBooleanModel.class, false); if (m == null) { return defaultValue; } return m.getAsBoolean(); } @Deprecated protected Content mandatoryContent(Map<String, TemplateModel> params, String key) throws TemplateModelException { return _param(params, key, ContentModel.class, true).asContent(); } @Deprecated protected Content content(Map<String, TemplateModel> params, String key, Content defaultValue) throws TemplateModelException { final ContentModel m = _param(params, key, ContentModel.class, false); if (m == null) { return defaultValue; } return m.asContent(); } protected Node node(Map<String, TemplateModel> params, String key, Node defaultValue) throws TemplateModelException { final ContentMapModel m = _param(params, key, ContentMapModel.class, false); if (m == null) { return defaultValue; } return m.getJCRNode(); } protected Object object(Map<String, TemplateModel> params, String key) throws TemplateModelException { final TemplateModel model = _param(params, key, TemplateModel.class, false); if (model == null) { return null; } return DeepUnwrap.unwrap(model); } protected Object mandatoryObject(Map<String, TemplateModel> params, String key) throws TemplateModelException { final TemplateModel model = _param(params, key, TemplateModel.class, true); return DeepUnwrap.unwrap(model); } protected List<String> mandatoryStringList(Map<String, TemplateModel> params, String key) throws TemplateModelException { final TemplateModel model = _param(params, key, TemplateModel.class, true); if (model instanceof TemplateScalarModel) { final String s = ((TemplateScalarModel) model).getAsString(); return Collections.singletonList(s); } else if (model instanceof TemplateSequenceModel) { final CollectionAndSequence coll = new CollectionAndSequence((TemplateSequenceModel) model); return unwrapStringList(coll, key); } else if (model instanceof TemplateCollectionModel) { final CollectionAndSequence coll = new CollectionAndSequence((TemplateCollectionModel) model); return unwrapStringList(coll, key); } else { throw new TemplateModelException(key + " must be a String, a Collection of Strings. Found " + model.getClass().getSimpleName() + "."); } } private List<String> unwrapStringList(CollectionAndSequence collAndSeq, String key) throws TemplateModelException { final List<String> list = new ArrayList<String>(); for (int i = 0; i < collAndSeq.size(); i++) { final TemplateModel tm = collAndSeq.get(i); if (tm instanceof TemplateScalarModel) { list.add(((TemplateScalarModel) tm).getAsString()); } else { throw new TemplateModelException("The '" + key + "' attribute must be a String or a Collection of Strings. Found Collection of " + tm.getClass().getSimpleName() + "."); } } return list; } protected <MT extends TemplateModel> MT _param(Map<String, TemplateModel> params, String key, Class<MT> type, boolean isMandatory) throws TemplateModelException { final boolean containsKey = params.containsKey(key); if (isMandatory && !containsKey) { throw new TemplateModelException("The '" + key + "' parameter is mandatory."); } // can't remove here: in case of parameter-less directive call, FreeMarker passes a read-only Map. final TemplateModel m = params.get(key); if (m != null && !type.isAssignableFrom(m.getClass())) { throw new TemplateModelException("The '" + key + "' parameter must be a " + type.getSimpleName() + " and is a " + m.getClass().getSimpleName() + "."); } if (m == null && containsKey) { // parameter is passed but null value ... (happens with content.nonExistingSubNode apparently) throw new TemplateModelException("The '" + key + "' parameter was passed but not or wrongly specified."); } if (containsKey) { params.remove(key); } return (MT) m; } /** * Init attributes common to all {@link AbstractContentTemplatingElement}. */ protected void initContentElement(Map<String, TemplateModel> params, AbstractContentTemplatingElement component) throws TemplateModelException { Node content = node(params, CONTENT_ATTRIBUTE, null); String workspace = string(params, WORKSPACE_ATTRIBUTE, null); String nodeIdentifier = string(params, UUID_ATTRIBUTE, null); String path = string(params, PATH_ATTRIBUTE, null); component.setContent(content); component.setWorkspace(workspace); component.setNodeIdentifier(nodeIdentifier); component.setPath(path); } }