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.container.CollectionGroup;
29  import org.kuali.rice.krad.uif.container.Group;
30  import org.kuali.rice.krad.uif.layout.LayoutManager;
31  import org.kuali.rice.krad.uif.layout.StackedLayoutManager;
32  import org.kuali.rice.krad.uif.util.ScriptUtils;
33  import org.kuali.rice.krad.uif.widget.Disclosure;
34  import org.kuali.rice.krad.uif.element.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, 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 }