001: /*
002: * This file is part of the Echo Web Application Framework (hereinafter "Echo").
003: * Copyright (C) 2002-2005 NextApp, Inc.
004: *
005: * Version: MPL 1.1/GPL 2.0/LGPL 2.1
006: *
007: * The contents of this file are subject to the Mozilla Public License Version
008: * 1.1 (the "License"); you may not use this file except in compliance with
009: * the License. You may obtain a copy of the License at
010: * http://www.mozilla.org/MPL/
011: *
012: * Software distributed under the License is distributed on an "AS IS" basis,
013: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
014: * for the specific language governing rights and limitations under the
015: * License.
016: *
017: * Alternatively, the contents of this file may be used under the terms of
018: * either the GNU General Public License Version 2 or later (the "GPL"), or
019: * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
020: * in which case the provisions of the GPL or the LGPL are applicable instead
021: * of those above. If you wish to allow use of your version of this file only
022: * under the terms of either the GPL or the LGPL, and not to allow others to
023: * use your version of this file under the terms of the MPL, indicate your
024: * decision by deleting the provisions above and replace them with the notice
025: * and other provisions required by the GPL or the LGPL. If you do not delete
026: * the provisions above, a recipient may use your version of this file under
027: * the terms of any one of the MPL, the GPL or the LGPL.
028: */
029:
030: package nextapp.echo2.webcontainer.syncpeer;
031:
032: import java.util.HashMap;
033: import java.util.Map;
034:
035: import org.w3c.dom.Document;
036: import org.w3c.dom.Element;
037:
038: import nextapp.echo2.app.Border;
039: import nextapp.echo2.app.Color;
040: import nextapp.echo2.app.Component;
041: import nextapp.echo2.app.Extent;
042: import nextapp.echo2.app.Font;
043: import nextapp.echo2.app.Insets;
044: import nextapp.echo2.app.ListBox;
045: import nextapp.echo2.app.list.AbstractListComponent;
046: import nextapp.echo2.app.list.ListCellRenderer;
047: import nextapp.echo2.app.list.ListModel;
048: import nextapp.echo2.app.list.ListSelectionModel;
049: import nextapp.echo2.app.list.StyledListCell;
050: import nextapp.echo2.app.update.ServerComponentUpdate;
051: import nextapp.echo2.webcontainer.ActionProcessor;
052: import nextapp.echo2.webcontainer.ComponentSynchronizePeer;
053: import nextapp.echo2.webcontainer.ContainerInstance;
054: import nextapp.echo2.webcontainer.FocusSupport;
055: import nextapp.echo2.webcontainer.PartialUpdateManager;
056: import nextapp.echo2.webcontainer.PropertyUpdateProcessor;
057: import nextapp.echo2.webcontainer.RenderContext;
058: import nextapp.echo2.webcontainer.propertyrender.BorderRender;
059: import nextapp.echo2.webcontainer.propertyrender.ColorRender;
060: import nextapp.echo2.webcontainer.propertyrender.ExtentRender;
061: import nextapp.echo2.webcontainer.propertyrender.FontRender;
062: import nextapp.echo2.webcontainer.propertyrender.InsetsRender;
063: import nextapp.echo2.webrender.ClientProperties;
064: import nextapp.echo2.webrender.ServerMessage;
065: import nextapp.echo2.webrender.Service;
066: import nextapp.echo2.webrender.WebRenderServlet;
067: import nextapp.echo2.webrender.output.CssStyle;
068: import nextapp.echo2.webrender.servermessage.DomUpdate;
069: import nextapp.echo2.webrender.servermessage.WindowUpdate;
070: import nextapp.echo2.webrender.service.JavaScriptService;
071: import nextapp.echo2.webrender.util.DomUtil;
072:
073: /**
074: * <code>ComponentSynchronizePeer</code> implementation for
075: * <code>AbstractListComponent</code>-based components.
076: * <p>
077: * This peer renders the content of list components in the
078: * <code>ServerMessage</code>'s initialization section
079: * (<code>ServerMessage.GROUP_ID_INIT</code>) such that a single
080: * rendering of content may be used by multiple list components
081: * if possible.
082: * <p>
083: * This class should not be extended or used by classes outside of the
084: * Echo framework.
085: */
086: public class ListComponentPeer implements ActionProcessor,
087: ComponentSynchronizePeer, FocusSupport, PropertyUpdateProcessor {
088:
089: /**
090: * Service to provide supporting JavaScript library.
091: */
092: public static final Service LIST_COMPONENT_SERVICE = JavaScriptService
093: .forResource("Echo.ListComponent",
094: "/nextapp/echo2/webcontainer/resource/js/ListComponent.js");
095:
096: static {
097: WebRenderServlet.getServiceRegistry().add(
098: LIST_COMPONENT_SERVICE);
099: }
100:
101: private static final String PROPERTY_SELECTION = "selection";
102:
103: // Default Colors
104: private static final Color DEFAULT_BACKGROUND = Color.WHITE;
105: private static final Color DEFAULT_FOREGROUND = Color.BLACK;
106:
107: // Default Sizes
108: private static final Extent DEFAULT_WIDTH = new Extent(100,
109: Extent.PERCENT);
110: private static final Insets DEFAULT_INSETS = new Insets(new Extent(
111: 0), new Extent(0));
112:
113: /**
114: * Key for <code>Connection</code> property containing a <code>Map</code> which maps
115: * RenderedModelData instances to ids.
116: */
117: private static final String RENDERED_MODEL_MAP_KEY = "nextapp.echo2.webcontainer.syncpeer.ListComponentPeer.RenderedModelMap";
118:
119: private PartialUpdateManager partialUpdateManager;
120:
121: /**
122: * A representation of the content of a list component, i.e., the
123: * contents of the <code>ListModel</code> after having been run through a
124: * <code>ListCellRenderer</code>.
125: */
126: private class RenderedModelData {
127:
128: /**
129: * An array containing the String values of the list component.
130: */
131: private String[] values;
132:
133: /**
134: * An array containing the CSS style values of the list component.
135: * May be null, or contain nulls representing list items without style.
136: */
137: private String[] styles;
138:
139: /**
140: * Cached hash code.
141: */
142: private int hashCode;
143:
144: /**
145: * Creates a new <code>RenderedModelData</code> instance.
146: *
147: * @param model the <code>ListModel</code>
148: * @param renderer the <code>ListCellRenderer</code>
149: */
150: private RenderedModelData(AbstractListComponent listComponent) {
151: ListModel model = listComponent.getModel();
152: ListCellRenderer renderer = listComponent.getCellRenderer();
153:
154: int size = model.size();
155: values = new String[size];
156: for (int i = 0; i < values.length; ++i) {
157: Object renderedValue = renderer
158: .getListCellRendererComponent(listComponent,
159: model.get(i), i);
160: values[i] = renderedValue.toString();
161:
162: if (renderedValue instanceof StyledListCell) {
163: StyledListCell styledListCell = (StyledListCell) renderedValue;
164: CssStyle itemStyle = new CssStyle();
165: ColorRender.renderToStyle(itemStyle, styledListCell
166: .getForeground(), styledListCell
167: .getBackground());
168: FontRender.renderToStyle(itemStyle, styledListCell
169: .getFont());
170: if (itemStyle.hasAttributes()) {
171: if (styles == null) {
172: styles = new String[size];
173: }
174: styles[i] = itemStyle.renderInline();
175: }
176: }
177: }
178: }
179:
180: /**
181: * @see java.lang.Object#hashCode()
182: */
183: public int hashCode() {
184: if (hashCode == 0) {
185: hashCode = values.length;
186: for (int i = 0; i < values.length; ++i) {
187: if (values[i] != null) {
188: hashCode ^= values[i].hashCode();
189: }
190: }
191: if (hashCode == 0) {
192: hashCode = 1;
193: }
194: }
195: return hashCode;
196: }
197:
198: /**
199: * @see java.lang.Object#equals(java.lang.Object)
200: */
201: public boolean equals(Object o) {
202: if (!(o instanceof RenderedModelData)) {
203: return false;
204: }
205: RenderedModelData that = (RenderedModelData) o;
206: if (this .values.length != that.values.length) {
207: return false;
208: }
209: for (int i = 0; i < this .values.length; ++i) {
210: if (!(this .values[i] == that.values[i] || (this .values[i] != null && this .values[i]
211: .equals(that.values[i])))) {
212: return false;
213: }
214: }
215: if (this .styles != null || that.styles != null) {
216: if (this .styles == null || that.styles == null) {
217: return false;
218: }
219: for (int i = 0; i < this .styles.length; ++i) {
220: if (!(this .styles[i] == that.styles[i] || (this .styles[i] != null && this .styles[i]
221: .equals(that.styles[i])))) {
222: return false;
223: }
224: }
225: }
226: return true;
227: }
228: }
229:
230: /**
231: * Default constructor.
232: */
233: public ListComponentPeer() {
234: super ();
235: partialUpdateManager = new PartialUpdateManager();
236: }
237:
238: /**
239: * Appends the base style to the given style based off of properties on the
240: * given <code>nextapp.echo2.app.AbstractListComponent</code>
241: *
242: * @param rc the relevant <code>RenderContext</code>
243: * @param listComponent the <code>nextapp.echo2.app.AbstractListComponent</code>
244: */
245: private CssStyle createListComponentCssStyle(RenderContext rc,
246: AbstractListComponent listComponent) {
247: CssStyle cssStyle = new CssStyle();
248:
249: boolean renderEnabled = listComponent.isRenderEnabled();
250:
251: Border border;
252: Color foreground, background;
253: Font font;
254: if (!renderEnabled) {
255: // Retrieve disabled style information.
256: background = (Color) listComponent
257: .getRenderProperty(AbstractListComponent.PROPERTY_DISABLED_BACKGROUND);
258: border = (Border) listComponent
259: .getRenderProperty(AbstractListComponent.PROPERTY_DISABLED_BORDER);
260: font = (Font) listComponent
261: .getRenderProperty(AbstractListComponent.PROPERTY_DISABLED_FONT);
262: foreground = (Color) listComponent
263: .getRenderProperty(AbstractListComponent.PROPERTY_DISABLED_FOREGROUND);
264:
265: // Fallback to normal styles.
266: if (background == null) {
267: background = (Color) listComponent.getRenderProperty(
268: AbstractListComponent.PROPERTY_BACKGROUND,
269: DEFAULT_BACKGROUND);
270: }
271: if (border == null) {
272: border = (Border) listComponent
273: .getRenderProperty(AbstractListComponent.PROPERTY_BORDER);
274: }
275: if (font == null) {
276: font = (Font) listComponent
277: .getRenderProperty(AbstractListComponent.PROPERTY_FONT);
278: }
279: if (foreground == null) {
280: foreground = (Color) listComponent.getRenderProperty(
281: AbstractListComponent.PROPERTY_FOREGROUND,
282: DEFAULT_FOREGROUND);
283: }
284: } else {
285: border = (Border) listComponent
286: .getRenderProperty(AbstractListComponent.PROPERTY_BORDER);
287: foreground = (Color) listComponent.getRenderProperty(
288: AbstractListComponent.PROPERTY_FOREGROUND,
289: DEFAULT_FOREGROUND);
290: background = (Color) listComponent.getRenderProperty(
291: AbstractListComponent.PROPERTY_BACKGROUND,
292: DEFAULT_BACKGROUND);
293: font = (Font) listComponent
294: .getRenderProperty(AbstractListComponent.PROPERTY_FONT);
295: }
296:
297: BorderRender.renderToStyle(cssStyle, border);
298: ColorRender.renderToStyle(cssStyle, foreground, background);
299: FontRender.renderToStyle(cssStyle, font);
300:
301: Extent width = (Extent) listComponent.getRenderProperty(
302: AbstractListComponent.PROPERTY_WIDTH, DEFAULT_WIDTH);
303: if (rc.getContainerInstance().getClientProperties().getBoolean(
304: ClientProperties.QUIRK_IE_SELECT_PERCENT_WIDTH)
305: && width.getUnits() == Extent.PERCENT) {
306: // Render default width.
307: width = null;
308: }
309: Extent height = (Extent) listComponent
310: .getRenderProperty(AbstractListComponent.PROPERTY_HEIGHT);
311: Insets insets = (Insets) listComponent.getRenderProperty(
312: AbstractListComponent.PROPERTY_INSETS, DEFAULT_INSETS);
313:
314: InsetsRender.renderToStyle(cssStyle, "padding", insets);
315: ExtentRender.renderToStyle(cssStyle, "width", width);
316: ExtentRender.renderToStyle(cssStyle, "height", height);
317: cssStyle.setAttribute("position", "relative");
318:
319: return cssStyle;
320: }
321:
322: /**
323: * Creates the rollover style based off of properties on the given
324: * <code>nextapp.echo2.app.AbstractListComponent</code>
325: *
326: * @param listComponent the <code>AbstractListComponent</code> instance
327: * @return the style
328: */
329: private CssStyle createRolloverCssStyle(
330: AbstractListComponent listComponent) {
331: CssStyle style = new CssStyle();
332: Color rolloverForeground = (Color) listComponent
333: .getRenderProperty(AbstractListComponent.PROPERTY_ROLLOVER_FOREGROUND);
334: Color rolloverBackground = (Color) listComponent
335: .getRenderProperty(AbstractListComponent.PROPERTY_ROLLOVER_BACKGROUND);
336: ColorRender.renderToStyle(style, rolloverForeground,
337: rolloverBackground);
338: FontRender
339: .renderToStyle(
340: style,
341: (Font) listComponent
342: .getRenderProperty(AbstractListComponent.PROPERTY_ROLLOVER_FONT));
343: return style;
344: }
345:
346: /**
347: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#getContainerId(nextapp.echo2.app.Component)
348: */
349: public String getContainerId(Component child) {
350: throw new UnsupportedOperationException(
351: "Component does not support children.");
352: }
353:
354: /**
355: * @see nextapp.echo2.webcontainer.ActionProcessor#processAction(nextapp.echo2.webcontainer.ContainerInstance,
356: * nextapp.echo2.app.Component, org.w3c.dom.Element)
357: */
358: public void processAction(ContainerInstance ci,
359: Component component, Element actionElement) {
360: ci.getUpdateManager().getClientUpdateManager()
361: .setComponentAction(component,
362: AbstractListComponent.INPUT_ACTION, null);
363: }
364:
365: /**
366: * @see nextapp.echo2.webcontainer.PropertyUpdateProcessor#processPropertyUpdate(nextapp.echo2.webcontainer.ContainerInstance,
367: * nextapp.echo2.app.Component, org.w3c.dom.Element)
368: */
369: public void processPropertyUpdate(ContainerInstance ci,
370: Component component, Element propertyElement) {
371: String propertyName = propertyElement
372: .getAttribute(PropertyUpdateProcessor.PROPERTY_NAME);
373: if (PROPERTY_SELECTION.equals(propertyName)) {
374: Element[] itemElements = DomUtil.getChildElementsByTagName(
375: propertyElement, "item");
376: int[] selectedIndices = new int[itemElements.length];
377: for (int i = 0; i < itemElements.length; ++i) {
378: selectedIndices[i] = Integer.parseInt(itemElements[i]
379: .getAttribute("index"));
380: }
381: ci
382: .getUpdateManager()
383: .getClientUpdateManager()
384: .setComponentProperty(
385: component,
386: AbstractListComponent.SELECTION_CHANGED_PROPERTY,
387: selectedIndices);
388: }
389: }
390:
391: /**
392: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderAdd(
393: * nextapp.echo2.webcontainer.RenderContext, nextapp.echo2.app.update.ServerComponentUpdate,
394: * java.lang.String, nextapp.echo2.app.Component)
395: */
396: public void renderAdd(RenderContext rc,
397: ServerComponentUpdate update, String targetId,
398: Component component) {
399: ServerMessage serverMessage = rc.getServerMessage();
400: serverMessage.addLibrary(LIST_COMPONENT_SERVICE.getId());
401: AbstractListComponent listComponent = (AbstractListComponent) component;
402: renderInitDirective(rc, listComponent, targetId);
403: }
404:
405: /**
406: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderDispose(nextapp.echo2.webcontainer.RenderContext,
407: * nextapp.echo2.app.update.ServerComponentUpdate, nextapp.echo2.app.Component)
408: */
409: public void renderDispose(RenderContext rc,
410: ServerComponentUpdate update, Component component) {
411: ServerMessage serverMessage = rc.getServerMessage();
412: serverMessage.addLibrary(LIST_COMPONENT_SERVICE.getId());
413: renderDisposeDirective(rc, (AbstractListComponent) component);
414: }
415:
416: /**
417: * Renders a directive to the outgoing <code>ServerMessage</code> to
418: * dispose the state of the <code>AbstractListComponent</code>, performing
419: * tasks such as unregistering event listeners on the client.
420: *
421: * @param rc the relevant <code>RenderContext</code>
422: * @param listComponent the <code>AbstractListComponent</code>
423: */
424: private void renderDisposeDirective(RenderContext rc,
425: AbstractListComponent listComponent) {
426: String elementId = ContainerInstance
427: .getElementId(listComponent);
428: ServerMessage serverMessage = rc.getServerMessage();
429: Element initElement = serverMessage.appendPartDirective(
430: ServerMessage.GROUP_ID_PREREMOVE,
431: "EchoListComponent.MessageProcessor", "dispose");
432: initElement.setAttribute("eid", elementId);
433: }
434:
435: /**
436: * Renders content for an <code>AbstractListComponent</code> (if necessary);
437: * returns the content id.
438: *
439: * @param rc the relevant <code>RenderContext</code>
440: * @param listComponent the <code>AbstractListComponent</code>
441: * @return the content id
442: */
443: private String renderContent(RenderContext rc,
444: AbstractListComponent listComponent) {
445: RenderedModelData renderedModelData = new RenderedModelData(
446: listComponent);
447:
448: Map renderedModelDataToIdMap = (Map) rc.getConnection()
449: .getProperty(RENDERED_MODEL_MAP_KEY);
450: if (renderedModelDataToIdMap == null) {
451: renderedModelDataToIdMap = new HashMap();
452: rc.getConnection().setProperty(RENDERED_MODEL_MAP_KEY,
453: renderedModelDataToIdMap);
454: }
455: String contentId = (String) renderedModelDataToIdMap
456: .get(renderedModelData);
457: if (contentId == null) {
458: contentId = Integer.toString(renderedModelDataToIdMap
459: .size());
460: renderedModelDataToIdMap.put(renderedModelData, contentId);
461: renderLoadContentDirective(rc, renderedModelData, contentId);
462: }
463: return contentId;
464: }
465:
466: /**
467: * Renders a directive to load the content (a rendered version of model)
468: * to the client.
469: *
470: * @param rc the relevant <code>RenderContext</code>
471: * @param renderedModelData the <code>RenderedModelData</code> object to render
472: * @param contentId the content id to associate with the rendered content
473: */
474: private void renderLoadContentDirective(RenderContext rc,
475: RenderedModelData renderedModelData, String contentId) {
476: ServerMessage serverMessage = rc.getServerMessage();
477: Element partElement = serverMessage.appendPartDirective(
478: ServerMessage.GROUP_ID_INIT,
479: "EchoListComponent.MessageProcessor", "load-content");
480: partElement.setAttribute("content-id", contentId);
481: Document document = serverMessage.getDocument();
482:
483: if (renderedModelData.styles != null) {
484: partElement.setAttribute("styled", "true");
485: }
486:
487: for (int i = 0; i < renderedModelData.values.length; ++i) {
488: Element itemElement = document.createElement("item");
489: itemElement.setAttribute("value",
490: renderedModelData.values[i] == null ? ""
491: : renderedModelData.values[i].toString());
492: if (renderedModelData.styles != null) {
493: itemElement.setAttribute("style",
494: renderedModelData.styles[i] == null ? ""
495: : renderedModelData.styles[i]
496: .toString());
497: }
498:
499: partElement.appendChild(itemElement);
500: }
501: }
502:
503: /**
504: * Renders a directive to the outgoing <code>ServerMessage</code> to
505: * render and intialize the state of a list component.
506: *
507: * @param rc the relevant <code>RenderContext</code>
508: * @param listComponent the component
509: * @param targetId the id of the container element
510: */
511: private void renderInitDirective(RenderContext rc,
512: AbstractListComponent listComponent, String targetId) {
513: String contentId = renderContent(rc, listComponent);
514: String elementId = ContainerInstance
515: .getElementId(listComponent);
516: ServerMessage serverMessage = rc.getServerMessage();
517: Document document = serverMessage.getDocument();
518:
519: Element initElement = serverMessage.appendPartDirective(
520: ServerMessage.GROUP_ID_UPDATE,
521: "EchoListComponent.MessageProcessor", "init");
522: initElement.setAttribute("container-eid", targetId);
523: initElement.setAttribute("eid", elementId);
524: initElement.setAttribute("content-id", contentId);
525:
526: initElement.setAttribute("enabled", listComponent
527: .isRenderEnabled() ? "true" : "false");
528:
529: if (listComponent.hasActionListeners()) {
530: initElement.setAttribute("server-notify", "true");
531: }
532:
533: CssStyle cssStyle = createListComponentCssStyle(rc,
534: listComponent);
535: initElement.setAttribute("style", cssStyle.renderInline());
536:
537: Boolean rolloverEnabled = (Boolean) listComponent
538: .getRenderProperty(AbstractListComponent.PROPERTY_ROLLOVER_ENABLED);
539: if (Boolean.TRUE.equals(rolloverEnabled)) {
540: CssStyle rolloverCssStyle = createRolloverCssStyle(listComponent);
541: initElement.setAttribute("rollover-style", rolloverCssStyle
542: .renderInline());
543: }
544:
545: boolean multipleSelect = false;
546: if (listComponent instanceof ListBox) {
547: initElement.setAttribute("type", "list-box");
548: ListBox listBox = (ListBox) listComponent;
549: if (listBox.getSelectionMode() == ListSelectionModel.MULTIPLE_SELECTION) {
550: initElement.setAttribute("multiple", "true");
551: multipleSelect = true;
552: }
553: }
554:
555: if (listComponent.isFocusTraversalParticipant()) {
556: initElement.setAttribute("tab-index", Integer
557: .toString(listComponent.getFocusTraversalIndex()));
558: } else {
559: initElement.setAttribute("tab-index", "-1");
560: }
561:
562: String toolTipText = (String) listComponent
563: .getRenderProperty(AbstractListComponent.PROPERTY_TOOL_TIP_TEXT);
564: if (toolTipText != null) {
565: initElement.setAttribute("tool-tip", toolTipText);
566: }
567:
568: // Render selection state.
569: ListSelectionModel selectionModel = listComponent
570: .getSelectionModel();
571: int minIndex = selectionModel.getMinSelectedIndex();
572: if (minIndex >= 0) {
573: if (multipleSelect) {
574: Element selectionElement = document
575: .createElement("selection");
576: int maxIndex = selectionModel.getMaxSelectedIndex();
577: for (int i = minIndex; i <= maxIndex; ++i) {
578: if (selectionModel.isSelectedIndex(i)) {
579: Element itemElement = document
580: .createElement("item");
581: itemElement.setAttribute("index", Integer
582: .toString(i));
583: selectionElement.appendChild(itemElement);
584: }
585: }
586: initElement.appendChild(selectionElement);
587: } else {
588: initElement.setAttribute("selection-index", Integer
589: .toString(minIndex));
590: }
591: }
592: }
593:
594: /**
595: * @see nextapp.echo2.webcontainer.FocusSupport#renderSetFocus(nextapp.echo2.webcontainer.RenderContext, nextapp.echo2.app.Component)
596: */
597: public void renderSetFocus(RenderContext rc, Component component) {
598: if (component.isEnabled()) {
599: WindowUpdate.renderSetFocus(rc.getServerMessage(),
600: ContainerInstance.getElementId(component));
601: }
602: }
603:
604: /**
605: * @see nextapp.echo2.webcontainer.ComponentSynchronizePeer#renderUpdate(
606: * nextapp.echo2.webcontainer.RenderContext, nextapp.echo2.app.update.ServerComponentUpdate, java.lang.String)
607: */
608: public boolean renderUpdate(RenderContext rc,
609: ServerComponentUpdate update, String targetId) {
610: // Determine if fully replacing the component is required.
611: if (partialUpdateManager.canProcess(rc, update)) {
612: partialUpdateManager.process(rc, update);
613: } else {
614: // Perform full update.
615: DomUpdate.renderElementRemove(rc.getServerMessage(),
616: ContainerInstance.getElementId(update.getParent()));
617: renderAdd(rc, update, targetId, update.getParent());
618: }
619:
620: return true;
621: }
622: }
|