View Javadoc

1   /**
2    * This file Copyright (c) 2010-2010 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.templatingcomponents.freemarker;
35  
36  import freemarker.core.CollectionAndSequence;
37  import freemarker.core.Environment;
38  import freemarker.template.TemplateBooleanModel;
39  import freemarker.template.TemplateCollectionModel;
40  import freemarker.template.TemplateDirectiveBody;
41  import freemarker.template.TemplateDirectiveModel;
42  import freemarker.template.TemplateException;
43  import freemarker.template.TemplateModel;
44  import freemarker.template.TemplateModelException;
45  import freemarker.template.TemplateScalarModel;
46  import freemarker.template.TemplateSequenceModel;
47  import info.magnolia.cms.beans.config.ServerConfiguration;
48  import info.magnolia.cms.core.AggregationState;
49  import info.magnolia.cms.core.Content;
50  import info.magnolia.context.MgnlContext;
51  import info.magnolia.freemarker.models.ContentModel;
52  import info.magnolia.module.templatingcomponents.AuthoringUiComponent;
53  
54  import java.io.IOException;
55  import java.util.ArrayList;
56  import java.util.Collections;
57  import java.util.List;
58  import java.util.Map;
59  
60  /**
61   * @author gjoseph
62   * @version $Revision: $ ($Author: $)
63   */
64  public abstract class AbstractDirective implements TemplateDirectiveModel {
65  
66      @SuppressWarnings("unchecked")
67      public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException {
68          final ServerConfiguration serverConfiguration = ServerConfiguration.getInstance();
69          final AggregationState aggregationState = MgnlContext.getAggregationState();
70          final AuthoringUiComponent uiComp = prepareUIComponent(serverConfiguration, aggregationState, env, params, loopVars, body);
71  
72          // prepareUIComponent should have removed the parameters it knows about.
73          if (!params.isEmpty()) {
74              throw new TemplateModelException("Unsupported parameter(s): " + params);
75          }
76  
77          uiComp.render(env.getOut());
78  
79          try {
80              doBody(env, body);
81          } finally {
82              uiComp.postRender();
83          }
84      }
85  
86      /**
87       * Implementations of this method should return a AuthoringUiComponent, prepared with the known parameters.
88       * If parameters have been grabbed using the methods provided by this class, they should be removed from
89       * 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)}
90       * will throw a TemplateModelException if there are leftover parameters.
91       *
92       * <strong>note:</strong> The current FreeMarker implementation passes a "new" Map which we can safely manipulate.
93       * is thrown away after the execution of the directive. When no parameters are passed, the Map is readonly, but it
94       * is otherwise a regular HashMap which has been instantiated shortly before the execution of the directive. However, since
95       * this behavior is not mandated by their API, nor documented (at the time of writing, with FreeMarker 2.3.16), we
96       * should exert caution. Unit tests hopefully cover this, so we'll be safe when updating to newer FreeMarker versions. 
97       */
98      protected abstract AuthoringUiComponent prepareUIComponent(ServerConfiguration serverCfg, AggregationState aggState, Environment env, Map<String, TemplateModel> params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateModelException, IOException;
99  
100     protected void doBody(Environment env, TemplateDirectiveBody body) throws TemplateException, IOException {
101         if (body != null) {
102             body.render(env.getOut());
103         }
104     }
105 
106     /**
107      * Utility method for directives who need to ensure they're used with or without a body.
108      * If the body is *optional*, this method shouldn't be used.
109      */
110     protected void checkBody(TemplateDirectiveBody body, boolean needsBody) throws TemplateModelException {
111         if ((body == null) == needsBody) {
112             throw new TemplateModelException("This directive " + (needsBody ? "needs a body" : "does not support a body"));
113         }
114     }
115 
116     protected String mandatoryString(Map<String, TemplateModel> params, String key) throws TemplateModelException {
117         return _param(params, key, TemplateScalarModel.class, true).getAsString();
118     }
119 
120     protected String string(Map<String, TemplateModel> params, String key, String defaultValue) throws TemplateModelException {
121         final TemplateScalarModel m = _param(params, key, TemplateScalarModel.class, false);
122         if (m == null) { // we've already checked if the param is mandatory already
123             return defaultValue;
124         }
125         return m.getAsString();
126     }
127 
128     protected boolean mandatoryBool(Map<String, TemplateModel> params, String key) throws TemplateModelException {
129         return _param(params, key, TemplateBooleanModel.class, true).getAsBoolean();
130     }
131 
132     protected boolean bool(Map<String, TemplateModel> params, String key, boolean defaultValue) throws TemplateModelException {
133         final TemplateBooleanModel m = _param(params, key, TemplateBooleanModel.class, false);
134         if (m == null) {
135             return defaultValue;
136         }
137         return m.getAsBoolean();
138     }
139 
140     protected Content mandatoryContent(Map<String, TemplateModel> params, String key) throws TemplateModelException {
141         return _param(params, key, ContentModel.class, true).asContent();
142     }
143 
144     protected Content content(Map<String, TemplateModel> params, String key, Content defaultValue) throws TemplateModelException {
145         final ContentModel m = _param(params, key, ContentModel.class, false);
146         if (m == null) {
147             return defaultValue;
148         }
149         return m.asContent();
150     }
151 
152     protected List<String> mandatoryStringList(Map<String, TemplateModel> params, String key) throws TemplateModelException {
153         final TemplateModel model = _param(params, key, TemplateModel.class, true);
154         if (model instanceof TemplateScalarModel) {
155             final String s = ((TemplateScalarModel) model).getAsString();
156             return Collections.singletonList(s);
157         } else if (model instanceof TemplateSequenceModel) {
158             final CollectionAndSequence coll = new CollectionAndSequence((TemplateSequenceModel) model);
159             return unwrapStringList(coll, key);
160         } else if (model instanceof TemplateCollectionModel) {
161             final CollectionAndSequence coll = new CollectionAndSequence((TemplateCollectionModel) model);
162             return unwrapStringList(coll, key);
163         } else {
164             throw new TemplateModelException(key + " must be a String, a Collection of Strings. Found " + model.getClass().getSimpleName() + ".");
165         }
166     }
167 
168     private List<String> unwrapStringList(CollectionAndSequence collAndSeq, String key) throws TemplateModelException {
169         final List<String> list = new ArrayList<String>();
170         for (int i = 0; i < collAndSeq.size(); i++) {
171             final TemplateModel tm = collAndSeq.get(i);
172             if (!(tm instanceof TemplateScalarModel)) {
173                 throw new TemplateModelException("The '" + key + "' attribute must be a String or a Collection of Strings. Found Collection of " + tm.getClass().getSimpleName() + ".");
174             } else {
175                 list.add(((TemplateScalarModel) tm).getAsString());
176             }
177         }
178         return list;
179     }
180 
181     protected <MT extends TemplateModel> MT _param(Map<String, TemplateModel> params, String key, Class<MT> type, boolean isMandatory) throws TemplateModelException {
182         final boolean containsKey = params.containsKey(key);
183         if (isMandatory && !containsKey) {
184             throw new TemplateModelException("The '" + key + "' parameter is mandatory.");
185 
186         }
187         // can't remove here: in case of parameter-less directive call, FreeMarker passes a read-only Map.
188         final TemplateModel m = params.get(key);
189         if (m != null && !type.isAssignableFrom(m.getClass())) {
190             throw new TemplateModelException("The '" + key + "' parameter must be a " + type.getSimpleName() + " and is a " + m.getClass().getSimpleName() + ".");
191         }
192         if (m == null && containsKey) {
193             // parameter is passed but null value ... (happens with content.nonExistingSubNode apparently)
194             throw new TemplateModelException("The '" + key + "' parameter was passed but not or wrongly specified.");
195         }
196         if (containsKey) {
197             params.remove(key);
198         }
199 
200         return (MT) m;
201     }
202 }