001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.freemarker;
017
018import java.io.IOException;
019import java.io.Writer;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.commons.lang.StringEscapeUtils;
026import org.kuali.rice.krad.uif.UifConstants;
027import org.kuali.rice.krad.uif.component.Component;
028import org.kuali.rice.krad.uif.container.CollectionGroup;
029import org.kuali.rice.krad.uif.container.Group;
030import org.kuali.rice.krad.uif.layout.LayoutManager;
031import org.kuali.rice.krad.uif.layout.StackedLayoutManager;
032import org.kuali.rice.krad.uif.util.ScriptUtils;
033import org.kuali.rice.krad.uif.widget.Disclosure;
034import org.kuali.rice.krad.uif.element.Pager;
035import org.kuali.rice.krad.uif.widget.Tooltip;
036import org.springframework.util.StringUtils;
037
038import freemarker.core.Environment;
039import freemarker.core.InlineTemplateUtils;
040import freemarker.core.Macro;
041import freemarker.ext.beans.BeansWrapper;
042import freemarker.template.ObjectWrapper;
043import freemarker.template.TemplateException;
044import freemarker.template.TemplateModel;
045import freemarker.template.TemplateModelException;
046
047/**
048 * Inline FreeMarker rendering utilities.
049 *
050 * @author Kuali Rice Team (rice.collab@kuali.org)
051 */
052public class FreeMarkerInlineRenderUtils {
053
054    /**
055     * Resolve a FreeMarker environment variable as a Java object.
056     *
057     * @param env The FreeMarker environment.
058     * @param name The name of the variable.
059     * @return The FreeMarker variable, resolved as a Java object.
060     * @see #resolve(Environment, String, Class) for the preferred means to resolve variables for
061     * inline rendering.
062     */
063    @SuppressWarnings("unchecked")
064    public static <T> T resolve(Environment env, String name) {
065        TemplateModel tm = resolveModel(env, name);
066        try {
067            return (T) getBeansWrapper(env).unwrap(tm);
068        } catch (TemplateModelException e) {
069            throw new IllegalArgumentException("Failed to unwrap " + name + ", template model " + tm, e);
070        }
071    }
072
073    /**
074     * Resolve a FreeMarker environment variable as a Java object, with type enforcement.
075     *
076     * <p>
077     * This method is the preferred means to resolve variables for inline rendering.
078     * </p>
079     *
080     * @param env The FreeMarker environment.
081     * @param name The name of the variable.
082     * @param type The expected type of the variable.
083     * @return The FreeMarker variable, resolved as a Java object of the given type.
084     */
085    public static <T> T resolve(Environment env, String name, Class<T> type) {
086        Object rv = resolve(env, name);
087
088        if ((rv instanceof Collection) && !Collection.class.isAssignableFrom(type)) {
089            Collection<?> rc = (Collection<?>) rv;
090            if (rc.isEmpty()) {
091                return null;
092            } else {
093                rv = rc.iterator().next();
094            }
095        }
096
097        if ("".equals(rv) && !String.class.equals(type)) {
098            return null;
099        } 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, boolean componentUpdate,
156            boolean includeSrc, Map<String, TemplateModel> tmplParms) throws TemplateException, IOException {
157        String dataJsScripts = "";
158        String templateJsScripts = "";
159
160        if (component == null) {
161            return;
162        }
163
164        String s;
165        Writer out = env.getOut();
166
167        if ((component.isRender() && (!component.isRetrieveViaAjax() || componentUpdate)) || (component
168                .getProgressiveRender() != null && !component.getProgressiveRender().equals("") && !component
169                .isProgressiveRenderViaAJAX() && !component.isProgressiveRenderAndRefresh())) {
170
171            if (StringUtils.hasText(s = component.getPreRenderContent())) {
172                out.write(s);
173            }
174
175            if (component.isSelfRendered()) {
176                out.write(component.getRenderedHtmlOutput());
177            } else {
178                if (includeSrc) {
179                    env.include(component.getTemplate(), env.getTemplate().getEncoding(), true);
180                }
181
182                Macro fmMacro = component.getTemplateName() == null ? null : (Macro) env.getMainNamespace().get(
183                        component.getTemplateName());
184
185                if (fmMacro == null) {
186                    // force inclusion of the source to see if we can get the macro
187                    env.include(component.getTemplate(), env.getTemplate().getEncoding(), true);
188                    fmMacro = component.getTemplateName() == null ? null : (Macro) env.getCurrentNamespace().get(
189                            component.getTemplateName());
190
191                    // if still missing throw an exception
192                    if (fmMacro == null) {
193                        throw new TemplateException("No macro found using " + component.getTemplateName(), env);
194                    }
195                }
196
197                Map<String, Object> args = new java.util.HashMap<String, Object>();
198                args.put(component.getComponentTypeName(), component);
199
200                if (tmplParms != null) {
201                    args.putAll(tmplParms);
202                }
203
204                if (StringUtils.hasText(body)) {
205                    args.put("body", body);
206                }
207
208                InlineTemplateUtils.invokeMacro(env, fmMacro, args, null);
209            }
210
211            if (StringUtils.hasText(s = component.getEventHandlerScript())) {
212                templateJsScripts += s;
213            }
214
215            if (StringUtils.hasText(s = component.getScriptDataAttributesJs())) {
216                dataJsScripts += s;
217            }
218
219            if (StringUtils.hasText(s = component.getPostRenderContent())) {
220                out.append(s);
221            }
222
223        }
224
225        if (componentUpdate || UifConstants.ViewStatus.RENDERED.equals(component.getViewStatus())) {
226            renderScript(dataJsScripts, component, UifConstants.RoleTypes.DATA_SCRIPT, out);
227            renderScript(templateJsScripts, component, null, out);
228            return;
229        }
230
231        String methodToCallOnRefresh = component.getMethodToCallOnRefresh();
232        if (!StringUtils.hasText(methodToCallOnRefresh)) {
233            methodToCallOnRefresh = "";
234        }
235
236        if ((!component.isRender() && (component.isProgressiveRenderViaAJAX() || component
237                .isProgressiveRenderAndRefresh() || component.isDisclosedByAction() || component.isRefreshedByAction()))
238                || component.isRetrieveViaAjax()) {
239            out.write("<span id=\"");
240            out.write(component.getId());
241            out.write("\" data-role=\"placeholder\" class=\"uif-placeholder "
242                    + component.getStyleClassesAsString()
243                    + "\"></span>");
244        }
245
246        if (StringUtils.hasText(component.getProgressiveRender())) {
247            for (String cName : component.getProgressiveDisclosureControlNames()) {
248                templateJsScripts += "var condition = function(){return ("
249                        + component.getProgressiveDisclosureConditionJs()
250                        + ");};setupProgressiveCheck('"
251                        + StringEscapeUtils.escapeJavaScript(cName)
252                        + "', '"
253                        + component.getId()
254                        + "', condition,"
255                        + component.isProgressiveRenderAndRefresh()
256                        + ", '"
257                        + methodToCallOnRefresh
258                        + "', "
259                        + ScriptUtils.translateValue(component.getFieldsToSendOnRefresh())
260                        + ");";
261            }
262
263            templateJsScripts += "hiddenInputValidationToggle('" + component.getId() + "');";
264        }
265
266        if (StringUtils.hasText(component.getConditionalRefresh())) {
267            for (String cName : component.getConditionalRefreshControlNames()) {
268                templateJsScripts += "var condition = function(){return ("
269                        + component.getConditionalRefreshConditionJs()
270                        + ");};setupRefreshCheck('"
271                        + StringEscapeUtils.escapeJavaScript(cName)
272                        + "', '"
273                        + component.getId()
274                        + "', condition,'"
275                        + methodToCallOnRefresh
276                        + "', "
277                        + ScriptUtils.translateValue(component.getFieldsToSendOnRefresh())
278                        + ");";
279            }
280        }
281
282        List<String> refreshWhenChanged = component.getRefreshWhenChangedPropertyNames();
283        if (refreshWhenChanged != null) {
284            for (String cName : refreshWhenChanged) {
285                templateJsScripts += "setupOnChangeRefresh('"
286                        + StringEscapeUtils.escapeJavaScript(cName)
287                        + "', '"
288                        + component.getId()
289                        + "','"
290                        + methodToCallOnRefresh
291                        + "', "
292                        + ScriptUtils.translateValue(component.getFieldsToSendOnRefresh())
293                        + ");";
294            }
295        }
296
297        renderScript(dataJsScripts, component, UifConstants.RoleTypes.DATA_SCRIPT, out);
298        renderScript(templateJsScripts, component, null, out);
299
300        renderTooltip(component, out);
301    }
302
303    /**
304     * Render a KRAD tooltip component.
305     *
306     * <p>
307     * This method originated as template.ftl, and supercedes the previous content of that template.
308     * </p>
309     *
310     * @param component The component to render a tooltip for.
311     * @param out The output writer to render to, typically from {@link Environment#getOut()}.
312     * @throws IOException If rendering is interrupted due to an I/O error.
313     */
314    public static void renderTooltip(Component component, Writer out) throws IOException {
315        Tooltip tt = component.getToolTip();
316        String script = "";
317        if (tt != null && StringUtils.hasText(tt.getTooltipContent())) {
318            String templateOptionsJSString = tt.getTemplateOptionsJSString();
319            script += "createTooltip('"
320                    + component.getId()
321                    + "', '"
322                    + tt.getTooltipContent()
323                    + "', "
324                    + (templateOptionsJSString == null ? "''" : templateOptionsJSString)
325                    + ", "
326                    + tt.isOnMouseHover()
327                    + ", "
328                    + tt.isOnFocus()
329                    + ");";
330
331            renderScript(script, component, null, out);
332        }
333    }
334
335    /**
336     * Render a KRAD script component.
337     *
338     * <p>
339     * This method originated as script.ftl, and supercedes the previous content of that template.
340     * </p>
341     *
342     * @param script The script to render.
343     * @param component The component the script is related to.
344     * @param out The output writer to render to, typically from {@link Environment#getOut()}.
345     * @throws IOException If rendering is interrupted due to an I/O error.
346     */
347    public static void renderScript(String script, Component component, String role, Writer out) throws IOException {
348        if (script == null || "".equals(script.trim())) {
349            return;
350        }
351        out.write("<input name=\"script\" type=\"hidden\" data-role=\"");
352        out.write(role == null ? "script" : role);
353        out.write("\" ");
354
355        if (component != null && component.getId() != null) {
356            out.write("data-for=\"");
357            out.write(component.getId());
358            out.write("\" ");
359        }
360
361        out.write("value=\"");
362        out.write(StringEscapeUtils.escapeHtml(script));
363        out.write("\" />");
364    }
365
366    /**
367     * Render common attributes for a KRAD component.
368     *
369     * <p>
370     * NOTICE: By KULRICE-10353 this method duplicates, but does not replace,
371     * krad/WEB-INF/ftl/lib/attrBuild.ftl. When updating this method, also update that template.
372     * </p>
373     *
374     * @param component The component to open a render attributes for.
375     * @param out The output writer to render to, typically from {@link Environment#getOut()}.
376     * @throws IOException If rendering is interrupted due to an I/O error.
377     */
378    public static void renderAttrBuild(Component component, Writer out) throws IOException {
379        String s = component.getStyleClassesAsString();
380        if (StringUtils.hasText(s)) {
381            out.write(" class=\"");
382            out.write(s);
383            out.write("\"");
384        }
385
386        s = component.getStyle();
387        if (StringUtils.hasText(s)) {
388            out.write(" style=\"");
389            out.write(s);
390            out.write("\"");
391        }
392
393        s = component.getTitle();
394        if (StringUtils.hasText(s)) {
395            out.write(" title=\"");
396            out.write(s);
397            out.write("\"");
398        }
399
400        s = component.getRole();
401        if (StringUtils.hasText(s)) {
402            out.write(" role=\"");
403            out.write(s);
404            out.write("\"");
405        }
406
407        s = component.getAriaAttributesAsString();
408        if (StringUtils.hasText(s)) {
409            out.write(s);
410        }
411    }
412
413    /**
414     * Render an open div tag for a component.
415     *
416     * <p>
417     * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
418     * content, so the open div and close div methods are implemented separately. Always call
419     * {@link #renderCloseDiv(Writer)} after rendering the &lt;div&gt; body related to this open
420     * tag.
421     * </p>
422     *
423     * <p>
424     * NOTICE: By KULRICE-10353 this method duplicates, but does not replace,
425     * krad/WEB-INF/ftp/lib/div.ftl. When updating this method, also update that template.
426     * </p>
427     *
428     * @param component The component to render a wrapper div for.
429     * @param out The output writer to render to, typically from {@link Environment#getOut()}.
430     * @throws IOException If rendering is interrupted due to an I/O error.
431     */
432    public static void renderOpenDiv(Component component, Writer out) throws IOException {
433        out.write("<div id=\"");
434        out.write(component.getId());
435        out.write("\"");
436        renderAttrBuild(component, out);
437        out.write(component.getSimpleDataAttributes());
438        out.write(">");
439    }
440
441    /**
442     * Render a close div tag for a component.
443     *
444     * <p>
445     * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
446     * content, so the open div and close div methods are implemented separately. Always call this
447     * method after rendering the &lt;div&gt; body related to and open tag rendered by
448     * {@link #renderOpenDiv(Component, Writer)}.
449     * </p>
450     *
451     * <p>
452     * NOTICE: By KULRICE-10353 this method duplicates, but does not replace,
453     * krad/WEB-INF/ftp/lib/div.ftl. When updating this method, also update that template.
454     * </p>
455     *
456     * @param out The output writer to render to, typically from {@link Environment#getOut()}.
457     * @throws IOException If rendering is interrupted due to an I/O error.
458     */
459    public static void renderCloseDiv(Writer out) throws IOException {
460        out.write("</div>");
461    }
462
463    /**
464     * Render open tags wrapping a group component.
465     *
466     * <p>
467     * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
468     * content, so the open and close methods are implemented separately. Always call
469     * {@link #renderCloseGroupWrap(Environment, Group)} after rendering the body related to a call to
470     * {@link #renderOpenGroupWrap(Environment, Group)}.
471     * </p>
472     *
473     * <p>
474     * This method originated as groupWrap.ftl, and supercedes the previous content of that
475     * template.
476     * </p>
477     *
478     * @param env The FreeMarker environment to use for rendering.
479     * @param group The group to render open wrapper tags for.
480     * @throws IOException If rendering is interrupted due to an I/O error.
481     * @throws TemplateException If FreeMarker rendering fails.
482     */
483    public static void renderOpenGroupWrap(Environment env, Group group) throws IOException, TemplateException {
484        Writer out = env.getOut();
485        renderTemplate(env, group.getHeader(), null, false, false, null);
486
487        if (group.isRenderLoading()) {
488            out.write("<div id=\"");
489            out.write(group.getId());
490            out.write("_disclosureContent\" data-role=\"placeholder\"> Loading... </div>");
491        } else {
492            Disclosure disclosure = group.getDisclosure();
493            if (disclosure != null && disclosure.isRender()) {
494                out.write("<div id=\"");
495                out.write(group.getId() + UifConstants.IdSuffixes.DISCLOSURE_CONTENT);
496                out.write("\" data-role=\"disclosureContent\" data-open=\"");
497                out.write(Boolean.toString(disclosure.isDefaultOpen()));
498                out.write("\" class=\"uif-disclosureContent\">");
499            }
500            renderTemplate(env, group.getInstructionalMessage(), null, false, false, null);
501        }
502    }
503
504    /**
505     * Render close tags wrapping a group component.
506     *
507     * <p>
508     * NOTE: Inline rendering performance is improved by *not* passing continuations for nested body
509     * content, so the open and close methods are implemented separately. Always call
510     * {@link #renderCloseGroupWrap(Environment, Group)} after rendering the body related to a call to
511     * {@link #renderOpenGroupWrap(Environment, Group)}.
512     * </p>
513     *
514     * <p>
515     * This method originated as groupWrap.ftl, and supercedes the previous content of that
516     * template.
517     * </p>
518     *
519     * @param env The FreeMarker environment to use for rendering.
520     * @param group The group to render open wrapper tags for.
521     * @throws IOException If rendering is interrupted due to an I/O error.
522     * @throws TemplateException If FreeMarker rendering fails.
523     */
524    public static void renderCloseGroupWrap(Environment env, Group group) throws IOException, TemplateException {
525        Writer out = env.getOut();
526
527        boolean renderLoading = group.isRenderLoading();
528        if (!renderLoading) {
529            renderTemplate(env, group.getFooter(), null, false, false, null);
530        }
531
532        Disclosure disclosure = group.getDisclosure();
533        if (disclosure != null && disclosure.isRender()) {
534            if (!renderLoading) {
535                out.write("</div>");
536            }
537            Map<String, TemplateModel> tmplParms = new HashMap<String, TemplateModel>();
538            tmplParms.put("parent", env.getObjectWrapper().wrap(group));
539            renderTemplate(env, disclosure, null, false, false, tmplParms);
540        }
541    }
542
543    /**
544     * Render a collection group inline.
545     *
546     * <p>
547     * This method originated as collectionGroup.ftl, and supercedes the previous content of that
548     * template.
549     * </p>
550     *
551     * @param group The collection group to render.
552     * @throws IOException If rendering is interrupted due to an I/O error.
553     * @throws TemplateException If FreeMarker rendering fails.
554     */
555    public static void renderCollectionGroup(Environment env,
556            CollectionGroup group) throws IOException, TemplateException {
557        renderOpenGroupWrap(env, group);
558
559        Map<String, TemplateModel> tmplParms = new HashMap<String, TemplateModel>();
560        tmplParms.put("componentId", env.getObjectWrapper().wrap(group.getId()));
561        renderTemplate(env, group.getCollectionLookup(), null, false, false, tmplParms);
562
563        if ("TOP".equals(group.getAddLinePlacement())) {
564            if (group.isRenderAddBlankLineButton()) {
565                renderTemplate(env, group.getAddBlankLineAction(), null, false, false, null);
566            }
567
568            if (group.isAddWithDialog()) {
569                renderTemplate(env, group.getAddWithDialogAction(), null, false, false, null);
570            }
571        }
572
573        LayoutManager layoutManager = group.getLayoutManager();
574        String managerTemplateName = layoutManager.getTemplateName();
575        List<? extends Component> items = group.getItems();
576
577        if ("uif_stacked".equals(managerTemplateName)) {
578            renderStacked(env, items, (StackedLayoutManager) layoutManager, group);
579        } else {
580            Macro fmMacro = (Macro) env.getMainNamespace().get(layoutManager.getTemplateName());
581
582            if (fmMacro == null) {
583                throw new TemplateException("No macro found using " + layoutManager.getTemplateName(), env);
584            }
585
586            Map<String, Object> args = new java.util.HashMap<String, Object>();
587            args.put("items", items);
588            args.put("manager", group.getLayoutManager());
589            args.put("container", group);
590            InlineTemplateUtils.invokeMacro(env, fmMacro, args, null);
591        }
592
593        if ("BOTTOM".equals(group.getAddLinePlacement())) {
594            if (group.isRenderAddBlankLineButton()) {
595                renderTemplate(env, group.getAddBlankLineAction(), null, false, false, null);
596            }
597
598            if (group.isAddWithDialog()) {
599                renderTemplate(env, group.getAddWithDialogAction(), null, false, false, null);
600            }
601        }
602
603        if (group.isAddWithDialog()) {
604            renderTemplate(env, group.getAddLineDialog(), null, false, false, null);
605        }
606
607        renderCloseGroupWrap(env, group);
608    }
609
610    /**
611     * Render a stacked collection inline.
612     *
613     * <p>
614     * This method originated as stacked.ftl, and supercedes the previous content of that
615     * template.
616     * </p>
617     *
618     * @param env The FreeMarker environment
619     * @param items List of items to render in a stacked layout
620     * @param manager Layout manager for the container
621     * @param container Container to render
622     * @throws IOException If rendering is interrupted due to an I/O error.
623     * @throws TemplateException If FreeMarker rendering fails.
624     */
625    public static void renderStacked(Environment env, List<? extends Component> items, StackedLayoutManager manager,
626            CollectionGroup container) throws IOException, TemplateException {
627        String s;
628        Writer out = env.getOut();
629
630        Pager pager = manager.getPagerWidget();
631        Map<String, TemplateModel> pagerTmplParms = null;
632        if (pager != null && container.isUseServerPaging()) {
633            pagerTmplParms = new HashMap<String, TemplateModel>();
634            renderTemplate(env, pager, null, false, false, pagerTmplParms);
635        }
636
637/*
638        out.write("<div id=\"");
639        out.write(manager.getId());
640        out.write("\"");
641
642        s = manager.getStyle();
643        if (StringUtils.hasText(s)) {
644            out.write(" style=\"");
645            out.write(s);
646            out.write("\"");
647        }
648
649        s = manager.getStyleClassesAsString();
650        if (StringUtils.hasText(s)) {
651            out.write(" class=\"");
652            out.write(s);
653            out.write("\"");
654        }
655
656        out.write(">");
657*/
658
659        Group wrapperGroup = manager.getWrapperGroup();
660        if (wrapperGroup != null) {
661            renderTemplate(env, wrapperGroup, null, false, false, null);
662        } else {
663            for (Group item : manager.getStackedGroups()) {
664                renderTemplate(env, item, null, false, false, null);
665            }
666        }
667
668        /*out.write("</div>");*/
669
670        if (pager != null && container.isUseServerPaging()) {
671            pagerTmplParms = new HashMap<String, TemplateModel>();
672            renderTemplate(env, pager, null, false, false, pagerTmplParms);
673        }
674    }
675
676}