View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.freemarker;
17  
18  import java.io.IOException;
19  import java.io.Writer;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  
25  import org.apache.commons.lang.StringEscapeUtils;
26  import org.kuali.rice.krad.uif.UifConstants;
27  import org.kuali.rice.krad.uif.component.Component;
28  import org.kuali.rice.krad.uif.component.ComponentBase;
29  import org.kuali.rice.krad.uif.container.CollectionGroup;
30  import org.kuali.rice.krad.uif.container.Group;
31  import org.kuali.rice.krad.uif.layout.LayoutManager;
32  import org.kuali.rice.krad.uif.layout.StackedLayoutManager;
33  import org.kuali.rice.krad.uif.widget.Disclosure;
34  import org.kuali.rice.krad.uif.widget.Pager;
35  import org.kuali.rice.krad.uif.widget.Tooltip;
36  import org.springframework.util.StringUtils;
37  
38  import freemarker.core.Environment;
39  import freemarker.core.InlineTemplateUtils;
40  import freemarker.core.Macro;
41  import freemarker.ext.beans.BeansWrapper;
42  import freemarker.template.ObjectWrapper;
43  import freemarker.template.TemplateException;
44  import freemarker.template.TemplateModel;
45  import freemarker.template.TemplateModelException;
46  
47  /**
48   * Inline FreeMarker rendering utilities.
49   * 
50   * @author Kuali Rice Team (rice.collab@kuali.org)
51   */
52  public class FreeMarkerInlineRenderUtils {
53  
54      /**
55       * Resolve a FreeMarker environment variable as a Java object.
56       * 
57       * @param env The FreeMarker environment.
58       * @param name The name of the variable.
59       * @return The FreeMarker variable, resolved as a Java object.
60       * @see #resolve(Environment, String, Class) for the preferred means to resolve variables for
61       *      inline rendering.
62       */
63      @SuppressWarnings("unchecked")
64      public static <T> T resolve(Environment env, String name) {
65          TemplateModel tm = resolveModel(env, name);
66          try {
67              return (T) getBeansWrapper(env).unwrap(tm);
68          } catch (TemplateModelException e) {
69              throw new IllegalArgumentException("Failed to unwrap " + name + ", template model " + tm, e);
70          }
71      }
72  
73      /**
74       * Resolve a FreeMarker environment variable as a Java object, with type enforcement.
75       * 
76       * <p>
77       * This method is the preferred means to resolve variables for inline rendering.
78       * </p>
79       * 
80       * @param env The FreeMarker environment.
81       * @param name The name of the variable.
82       * @param type The expected type of the variable.
83       * @return The FreeMarker variable, resolved as a Java object of the given type.
84       */
85      public static <T> T resolve(Environment env, String name, Class<T> type) {
86          Object rv = resolve(env, name);
87  
88          if ((rv instanceof Collection) && !Collection.class.isAssignableFrom(type)) {
89              Collection<?> rc = (Collection<?>) rv;
90              if (rc.isEmpty()) {
91                  return null;
92              } else {
93                  rv = rc.iterator().next();
94              }
95          }
96  
97          if ("".equals(rv) && !String.class.equals(type)) {
98              return null;
99          } else {
100             return type.cast(rv);
101         }
102     }
103 
104     /**
105      * Get the object wrapper from the FreeMarker environment, as a {@link BeansWrapper}.
106      * 
107      * @param env The FreeMarker environment.
108      * @return The object wrapper from the FreeMarker environment, type-cast as {@link BeansWrapper}
109      *         .
110      */
111     public static BeansWrapper getBeansWrapper(Environment env) {
112         ObjectWrapper wrapper = env.getObjectWrapper();
113 
114         if (!(wrapper instanceof BeansWrapper)) {
115             throw new UnsupportedOperationException("FreeMarker environment uses unsupported ObjectWrapper " + wrapper);
116         }
117 
118         return (BeansWrapper) wrapper;
119     }
120 
121     /**
122      * Resovle a FreeMarker variable as a FreeMarker template model object.
123      * 
124      * @param env The FreeMarker environment.
125      * @param name The name of the variable.
126      * @return The FreeMarker variable, resolved as a FreeMarker template model object.
127      * @see #resolve(Environment, String, Class) for the preferred means to resolve variables for
128      *      inline rendering.
129      */
130     public static TemplateModel resolveModel(Environment env, String name) {
131         try {
132             return env.getVariable(name);
133         } catch (TemplateModelException e) {
134             throw new IllegalArgumentException("Failed to resolve " + name + " in current freemarker environment", e);
135         }
136     }
137 
138     /**
139      * Render a KRAD component template inline.
140      * 
141      * <p>
142      * This method originated as template.ftl, and supercedes the previous content of that template.
143      * </p>
144      * 
145      * @param env The FreeMarker environment.
146      * @param component The component to render a template for.
147      * @param body The nested body.
148      * @param componentUpdate True if this is an update, false for full view.
149      * @param includeSrc True to include the template source in the environment when rendering,
150      *        false to skip inclusion.
151      * @param tmplParms Additional parameters to pass to the template macro.
152      * @throws TemplateException If FreeMarker rendering fails.
153      * @throws IOException If rendering is interrupted due to an I/O error.
154      */
155     public static void renderTemplate(Environment env, Component component, String body,
156             boolean componentUpdate, boolean includeSrc, Map<String, TemplateModel> tmplParms)
157             throws TemplateException, IOException {
158 
159         if (component == null) {
160             return;
161         }
162 
163         String s;
164         Writer out = env.getOut();
165         if ((component.isRender() && (!component.isRetrieveViaAjax() || componentUpdate))
166                 ||
167                 (component.getProgressiveRender() != null && !component.getProgressiveRender().equals("")
168                         && !component.isProgressiveRenderViaAJAX() && !component.isProgressiveRenderAndRefresh())) {
169 
170             if (StringUtils.hasText(s = component.getPreRenderContent())) {
171                 out.write(StringEscapeUtils.escapeHtml(s));
172             }
173 
174             if (component.isSelfRendered()) {
175                 out.write(component.getRenderedHtmlOutput());
176             } else {
177                 if (includeSrc) {
178                     env.include(component.getTemplate(), env.getTemplate().getEncoding(), true);
179                 }
180 
181                 Macro fmMacro = (Macro) env.getMainNamespace().get(component.getTemplateName());
182 
183                 if (fmMacro == null) {
184                     throw new TemplateException("No macro found using " + component.getTemplateName(), env);
185                 }
186 
187                 Map<String, Object> args = new java.util.HashMap<String, Object>();
188                 args.put(component.getComponentTypeName(), component);
189 
190                 if (tmplParms != null) {
191                     args.putAll(tmplParms);
192                 }
193 
194                 if (StringUtils.hasText(body)) {
195                     args.put("body", body);
196                 }
197 
198                 InlineTemplateUtils.invokeMacro(env, fmMacro, args, null);
199             }
200 
201             if (StringUtils.hasText(s = component.getEventHandlerScript())) {
202                 renderScript(s, component, null, out);
203             }
204 
205             if (StringUtils.hasText(s = component.getPostRenderContent())) {
206                 out.append(StringEscapeUtils.escapeHtml(s));
207             }
208 
209         }
210 
211         if (componentUpdate) {
212             return;
213         }
214 
215         String methodToCallOnRefresh = ((ComponentBase) component).getMethodToCallOnRefresh();
216         if (!StringUtils.hasText(methodToCallOnRefresh)) {
217             methodToCallOnRefresh = "";
218         }
219 
220         if (StringUtils.hasText(s = component.getProgressiveRender())) {
221             if (!component.isRender()
222                     && (component.isProgressiveRenderViaAJAX() || component.isProgressiveRenderAndRefresh())) {
223                 out.write("<span id=\"");
224                 out.write(component.getId());
225                 out.write("\" data-role=\"placeholder\" class=\"uif-placeholder\"></span>");
226             }
227 
228             for (String cName : component.getProgressiveDisclosureControlNames()) {
229                 renderScript(
230                         "var condition = function(){return ("
231                                 + component.getProgressiveDisclosureConditionJs()
232                                 + ");};setupProgressiveCheck('" + StringEscapeUtils.escapeJavaScript(cName)
233                                 + "', '" + component.getId() + "', '" + component.getBaseId() + "', condition,"
234                                 + component.isProgressiveRenderAndRefresh() + ", '"
235                                 + methodToCallOnRefresh + "');"
236                         , component, null, out);
237             }
238 
239             renderScript("hiddenInputValidationToggle('" + component.getId() + "');", null, null, out);
240         }
241 
242         if ((component.isProgressiveRenderViaAJAX() && !StringUtils.hasLength(component.getProgressiveRender())) ||
243                 (!component.isRender() && (component.isDisclosedByAction() || component.isRefreshedByAction())) ||
244                 component.isRetrieveViaAjax()) {
245             out.write("<span id=\"");
246             out.write(component.getId());
247             out.write("\" data-role=\"placeholder\" class=\"uif-placeholder\"></span>");
248         }
249 
250         if (StringUtils.hasText(component.getConditionalRefresh())) {
251             for (String cName : component.getConditionalRefreshControlNames()) {
252                 renderScript(
253                         "var condition = function(){return ("
254                                 + component.getConditionalRefreshConditionJs()
255                                 + ");};setupRefreshCheck('" + StringEscapeUtils.escapeJavaScript(cName) + "', '"
256                                 + component.getId() + "', condition,'"
257                                 + methodToCallOnRefresh + "');", null, null, out);
258             }
259         }
260 
261         List<String> refreshWhenChanged = component.getRefreshWhenChangedPropertyNames();
262         if (refreshWhenChanged != null) {
263             for (String cName : refreshWhenChanged) {
264                 renderScript(
265                         "setupOnChangeRefresh('" + StringEscapeUtils.escapeJavaScript(cName) + "', '"
266                                 + component.getId()
267                                 + "','" + methodToCallOnRefresh + "');", null, null, out);
268             }
269         }
270 
271         renderTooltip(component, out);
272     }
273 
274     /**
275      * Render a KRAD tooltip component.
276      * 
277      * <p>
278      * This method originated as template.ftl, and supercedes the previous content of that template.
279      * </p>
280      * 
281      * @param component The component to render a tooltip for.
282      * @param out The output writer to render to, typically from {@link Environment#getOut()}.
283      * @throws IOException If rendering is interrupted due to an I/O error.
284      */
285     public static void renderTooltip(Component component, Writer out) throws IOException {
286         Tooltip tt = component.getToolTip();
287         if (tt != null && StringUtils.hasText(tt.getTooltipContent())) {
288             String templateOptionsJSString = tt.getTemplateOptionsJSString();
289             renderScript("createTooltip('" + component.getId() + "', '" + tt.getTooltipContent() + "', "
290                     + (templateOptionsJSString == null ? "''" : templateOptionsJSString) + ", " + tt.isOnMouseHover()
291                     + ", " + tt.isOnFocus() + ");", component, null, out);
292             renderScript("addAttribute('" + component.getId() + "', 'class', 'uif-tooltip', true);", component, null,
293                     out);
294         }
295     }
296 
297     /**
298      * Render a KRAD script component.
299      * 
300      * <p>
301      * This method originated as script.ftl, and supercedes the previous content of that template.
302      * </p>
303      * 
304      * @param script The script to render.
305      * @param component The component the script is related to.
306      * @param out The output writer to render to, typically from {@link Environment#getOut()}.
307      * @throws IOException If rendering is interrupted due to an I/O error.
308      */
309     public static void renderScript(String script, Component component, String role, Writer out) throws IOException {
310         if (script == null || "".equals(script.trim()))
311             return;
312         out.write("<input name=\"script\" type=\"hidden\" data-role=\"");
313         out.write(role == null ? "script" : role);
314         out.write("\" ");
315 
316         if (component != null && component.getId() != null) {
317             out.write("data-for=\"");
318             out.write(component.getId());
319             out.write("\" ");
320         }
321 
322         out.write("value=\"");
323         out.write(StringEscapeUtils.escapeHtml(script));
324         out.write("\" />");
325     }
326 
327     /**
328      * Render common attributes for a KRAD component.
329      * 
330      * <p>
331      * NOTICE: By KULRICE-10353 this method duplicates, but does not replace,
332      * krad/WEB-INF/ftl/lib/attrBuild.ftl. When updating this method, also update that template.
333      * </p>
334      * 
335      * @param component The component to open a render attributes for.
336      * @param out The output writer to render to, typically from {@link Environment#getOut()}.
337      * @throws IOException If rendering is interrupted due to an I/O error.
338      */
339     public static void renderAttrBuild(Component component, Writer out) throws IOException {
340         String s;
341         if (component instanceof ComponentBase) {
342             ComponentBase componentBase = (ComponentBase) component;
343             if (StringUtils.hasText(s = componentBase.getStyleClassesAsString())) {
344                 out.write(" class=\"");
345                 out.write(s);
346                 out.write("\"");
347             }
348         }
349 
350         if (StringUtils.hasText(s = component.getStyle())) {
351             out.write(" style=\"");
352             out.write(s);
353             out.write("\"");
354         }
355 
356         if (StringUtils.hasText(s = component.getTitle())) {
357             out.write(" title=\"");
358             out.write(s);
359             out.write("\"");
360         }
361     }
362 
363     /**
364      * Render an open div tag for a component.
365      * 
366      * <p>
367      * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
368      * content, so the open div and close div methods are implemented separately. Always call
369      * {@link #renderCloseDiv(Writer)} after rendering the &lt;div&gt; body related to this open
370      * tag.
371      * </p>
372      * 
373      * <p>
374      * NOTICE: By KULRICE-10353 this method duplicates, but does not replace,
375      * krad/WEB-INF/ftp/lib/div.ftl. When updating this method, also update that template.
376      * </p>
377      * 
378      * @param component The component to render a wrapper div for.
379      * @param out The output writer to render to, typically from {@link Environment#getOut()}.
380      * @throws IOException If rendering is interrupted due to an I/O error.
381      */
382     public static void renderOpenDiv(Component component, Writer out) throws IOException {
383         out.write("<div id=\"");
384         out.write(component.getId());
385         out.write("\"");
386         renderAttrBuild(component, out);
387         out.write(component.getSimpleDataAttributes());
388         out.write(">");
389     }
390 
391     /**
392      * Render a close div tag for a component.
393      * 
394      * <p>
395      * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
396      * content, so the open div and close div methods are implemented separately. Always call this
397      * method after rendering the &lt;div&gt; body related to and open tag rendered by
398      * {@link #renderOpenDiv(Component, Writer)}.
399      * </p>
400      * 
401      * <p>
402      * NOTICE: By KULRICE-10353 this method duplicates, but does not replace,
403      * krad/WEB-INF/ftp/lib/div.ftl. When updating this method, also update that template.
404      * </p>
405      * 
406      * @param component The component to render a wrapper div for.
407      * @param out The output writer to render to, typically from {@link Environment#getOut()}.
408      * @throws IOException If rendering is interrupted due to an I/O error.
409      */
410     public static void renderCloseDiv(Writer out) throws IOException {
411         out.write("</div>");
412     }
413 
414     /**
415      * Render open tags wrapping a group component.
416      * 
417      * <p>
418      * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
419      * content, so the open and close methods are implemented separately. Always call
420      * {@link #renderCloseGroupWrap(Writer)} after rendering the body related to a call to
421      * {@link #renderOpenGroupWrap(Environment, Group)}.
422      * </p>
423      * 
424      * <p>
425      * This method originated as groupWrap.ftl, and supercedes the previous content of that
426      * template.
427      * </p>
428      * 
429      * @param env The FreeMarker environment to use for rendering.
430      * @param group The group to render open wrapper tags for.
431      * @throws IOException If rendering is interrupted due to an I/O error.
432      * @throws TemplateException If FreeMarker rendering fails.
433      */
434     public static void renderOpenGroupWrap(Environment env, Group group) throws IOException, TemplateException {
435         Writer out = env.getOut();
436         renderOpenDiv(group, out);
437         renderTemplate(env, group.getHeader(), null, false, false, null);
438 
439         if (group.isRenderLoading()) {
440             out.write("<div id=\"");
441             out.write(group.getId());
442             out.write("_disclosureContent\" data-role=\"placeholder\"> Loading... </div>");
443         } else {
444             Disclosure disclosure = group.getDisclosure();
445             if (disclosure != null && disclosure.isRender()) {
446                 out.write("<div id=\"");
447                 out.write(group.getId() + UifConstants.IdSuffixes.DISCLOSURE_CONTENT);
448                 out.write("\" data-role=\"disclosureContent\" data-open=\"");
449                 out.write(Boolean.toString(disclosure.isDefaultOpen()));
450                 out.write("\" class=\"uif-disclosureContent\">");
451             }
452             renderTemplate(env, group.getValidationMessages(), null, false, false, null);
453             renderTemplate(env, group.getInstructionalMessage(), null, false, false, null);
454         }
455     }
456 
457     /**
458      * Render close tags wrapping a group component.
459      * 
460      * <p>
461      * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
462      * content, so the open and close methods are implemented separately. Always call
463      * {@link #renderCloseGroupWrap(Writer)} after rendering the body related to a call to
464      * {@link #renderOpenGroupWrap(Environment, Group)}.
465      * </p>
466      * 
467      * <p>
468      * This method originated as groupWrap.ftl, and supercedes the previous content of that
469      * template.
470      * </p>
471      * 
472      * @param env The FreeMarker environment to use for rendering.
473      * @param group The group to render open wrapper tags for.
474      * @throws IOException If rendering is interrupted due to an I/O error.
475      * @throws TemplateException If FreeMarker rendering fails.
476      */
477     public static void renderCloseGroupWrap(Environment env, Group group) throws IOException, TemplateException {
478         Writer out = env.getOut();
479 
480         boolean renderLoading = group.isRenderLoading();
481         if (!renderLoading) {
482             renderTemplate(env, group.getFooter(), null, false, false, null);
483         }
484 
485         Disclosure disclosure = group.getDisclosure();
486         if (disclosure != null && disclosure.isRender()) {
487             if (!renderLoading) {
488                 out.write("</div>");
489             }
490             Map<String, TemplateModel> tmplParms = new HashMap<String, TemplateModel>();
491             tmplParms.put("parent", env.getObjectWrapper().wrap(group));
492             renderTemplate(env, disclosure, null, false, false, tmplParms);
493         }
494 
495         renderCloseDiv(out);
496     }
497 
498     /**
499      * Render a collection group inline.
500      * 
501      * <p>
502      * This method originated as collectionGroup.ftl, and supercedes the previous content of that
503      * template.
504      * </p>
505      * 
506      * @param component The component to render a wrapper div for.
507      * @param group The collection group to render.
508      * @throws IOException If rendering is interrupted due to an I/O error.
509      * @throws TemplateException If FreeMarker rendering fails.
510      */
511     public static void renderCollectionGroup(Environment env, CollectionGroup group) throws IOException,
512             TemplateException {
513         renderOpenGroupWrap(env, group);
514 
515         Map<String, TemplateModel> tmplParms = new HashMap<String, TemplateModel>();
516         tmplParms.put("componentId", env.getObjectWrapper().wrap(group.getId()));
517         renderTemplate(env, group.getCollectionLookup(), null, false, false, tmplParms);
518 
519         if ("TOP".equals(group.getAddLinePlacement())) {
520             if (group.isRenderAddBlankLineButton()) {
521                 renderTemplate(env, group.getAddBlankLineAction(), null, false, false, null);
522             }
523 
524             if (group.isAddViaLightBox()) {
525                 renderTemplate(env, group.getAddViaLightBoxAction(), null, false, false, null);
526             }
527         }
528 
529         LayoutManager layoutManager = group.getLayoutManager();
530         String managerTemplateName = layoutManager.getTemplateName();
531         List<? extends Component> items = group.getItems();
532 
533         if ("uif_stacked".equals(managerTemplateName)) {
534             renderStacked(env, items, (StackedLayoutManager) layoutManager, group);
535         } else {
536             Macro fmMacro = (Macro) env.getMainNamespace().get(layoutManager.getTemplateName());
537 
538             if (fmMacro == null) {
539                 throw new TemplateException("No macro found using " + layoutManager.getTemplateName(), env);
540             }
541 
542             Map<String, Object> args = new java.util.HashMap<String, Object>();
543             args.put("items", items);
544             args.put("manager", group.getLayoutManager());
545             args.put("container", group);
546             InlineTemplateUtils.invokeMacro(env, fmMacro, args, null);
547         }
548 
549         if ("BOTTOM".equals(group.getAddLinePlacement())) {
550             if (group.isRenderAddBlankLineButton()) {
551                 renderTemplate(env, group.getAddBlankLineAction(), null, false, false, null);
552             }
553 
554             if (group.isAddViaLightBox()) {
555                 renderTemplate(env, group.getAddViaLightBoxAction(), null, false, false, null);
556             }
557         }
558 
559         renderCloseGroupWrap(env, group);
560     }
561 
562     /**
563      * Render a stacked collection inline.
564      * 
565      * <p>
566      * This method originated as stacked.ftl, and supercedes the previous content of that
567      * template.
568      * </p>
569      * 
570      * @param component The component to render a wrapper div for.
571      * @param group The collection group to render.
572      * @throws IOException If rendering is interrupted due to an I/O error.
573      * @throws TemplateException If FreeMarker rendering fails.
574      */
575     public static void renderStacked(Environment env, List<? extends Component> items, StackedLayoutManager manager,
576             CollectionGroup container) throws IOException, TemplateException {
577         String s;
578         Writer out = env.getOut();
579 
580         Pager pager = manager.getPagerWidget();
581         Map<String, TemplateModel> pagerTmplParms = null;
582         if (pager != null && container.isUseServerPaging()) {
583             pagerTmplParms = new HashMap<String, TemplateModel>();
584             pagerTmplParms.put("parent", env.getObjectWrapper().wrap(container));
585             renderTemplate(env, pager, null, false, false, pagerTmplParms);
586         }
587 
588         out.write("<div id=\"");
589         out.write(manager.getId());
590         out.write("\"");
591 
592         if (StringUtils.hasText(s = manager.getStyle())) {
593             out.write(" style=\"");
594             out.write(s);
595             out.write("\"");
596         }
597 
598         if (StringUtils.hasText(s = manager.getStyleClassesAsString())) {
599             out.write(" class=\"");
600             out.write(s);
601             out.write("\"");
602         }
603 
604         out.write(">");
605 
606         Group wrapperGroup = manager.getWrapperGroup();
607         if (wrapperGroup != null) {
608             renderTemplate(env, wrapperGroup, null, false, false, null);
609         } else {
610             for (Group item : manager.getStackedGroups()) {
611                 renderTemplate(env, item, null, false, false, null);
612             }
613         }
614 
615         out.write("</div>");
616 
617         if (pager != null && container.isUseServerPaging()) {
618             pagerTmplParms = new HashMap<String, TemplateModel>();
619             pagerTmplParms.put("parent", env.getObjectWrapper().wrap(container));
620             renderTemplate(env, pager, null, false, false, pagerTmplParms);
621         }
622     }
623 
624 }