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 <div> 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 <div> 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}