001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package com.sun.rave.web.ui.faces;
042:
043: import java.util.ArrayList;
044: import java.util.List;
045: import javax.faces.el.EvaluationException;
046: import javax.faces.el.PropertyNotFoundException;
047: import javax.faces.el.PropertyResolver;
048: import javax.faces.model.SelectItem;
049: import com.sun.data.provider.DataProvider;
050: import com.sun.data.provider.FieldKey;
051: import com.sun.data.provider.RowKey;
052: import com.sun.data.provider.TableDataProvider;
053: import com.sun.data.provider.impl.TableRowDataProvider;
054: import com.sun.rave.web.ui.model.Option;
055:
056: /**
057: * <p><code>DataProviderPropertyResolver</code> is a
058: * <code>PropertyResolver</code> implementation that, if the <code>base</code>
059: * parameter is a {@link DataProvider}, passes calls to <code>getValue()</code>,
060: * <code>getType()</code>, <code>isReadOnly()</code>, and <code>setValue()</code>
061: * to the corresponding {@link DataProvider} instance. Otherwise, it follows
062: * the standard JSF decorator pattern and delegates processing to the decorated
063: * <code>PropertyResolver</code> instance.</p>
064: *
065: * <p>These expressions are supported:</p>
066: *
067: * <p>
068: * <code>#{...myDataProvider.value.FIELD_ID}</code><br>
069: * <code>#{...myDataProvider.value['FIELD_ID']}</code><br>
070: * --> binds to the value of the {@link FieldKey} corresponding to 'FIELD_ID' in
071: * a DataProvider *or* the cursor row of a TableDataProvider. If the specified
072: * FIELD_ID does not correspond to a FieldKey in the DataProvider, this property
073: * resolver will throw a PropertyNotFoundException, and include any nested
074: * exceptions.</p>
075: *
076: * <p>
077: * <code>#{...myDataProvider.value[':ROWKEY:']}</code><br>
078: * --> binds to the 'cursorRow' {@link RowKey} of a TableDataProvider or the
079: * 'tableRow' RowKey of a TableRowDataProvider. If the DataProvider is not one
080: * of these, this binds to nothing. Note that cursor or tableRow can be
081: * *driven* by this binding. It is not read-only.</p>
082: *
083: * <p>
084: * <code>#{...myDataProvider.value[':ROWID:']}</code><br>
085: * --> binds to the 'cursorRow' {@link RowKey}'s ID (String) of a
086: * TableDataProvider or the 'tableRow' RowKey's ID of a TableRowDataProvider.
087: * If the DataProvider is not one of these, this binds to nothing. Note that
088: * cursor or tableRow can be *driven* by this binding. It is not read-only.</p>
089: *
090: * <p>
091: * <code>#{...myDataProvider.selectItems.FIELD_ID}</code><br>
092: * <code>#{...myDataProvider.selectItems['FIELD_ID']}</code><br>
093: * <code>#{...myDataProvider.selectItems['VALUE_FIELD_ID,LABEL_FIELD_ID']}</code><br>
094: * <code>#{...myDataProvider.selectItems['VALUE_FIELD_ID,LABEL_FIELD_ID,DESC_FIELD_ID']}</code><br>
095: * <code>#{...myDataProvider.selectItems[':ROWKEY:,:ROWKEY:,:ROWKEY:']}</code><br>
096: * <code>#{...myDataProvider.selectItems[':ROWID:,:ROWID:,:ROWID:']}</code><br>
097: * --> binds to an array of {@link SelectItem} generated by scanning the rows of
098: * the TableDataProvider (without moving the cursor). If the base object is a
099: * DataProvider, but not a TableDataProvider, the resulting SelectItem[] will
100: * have one element. Note that the special :ROWKEY: and :ROWID: field IDs can
101: * be used here.</p>
102: *
103: * <p>
104: * <code>#{...myDataProvider.options.FIELD_ID}</code><br>
105: * <code>#{...myDataProvider.options['FIELD_ID']}</code><br>
106: * <code>#{...myDataProvider.options['VALUE_FIELD_ID,LABEL_FIELD_ID']}</code><br>
107: * <code>#{...myDataProvider.options['VALUE_FIELD_ID,LABEL_FIELD_ID,DESC_FIELD_ID']}</code><br>
108: * <code>#{...myDataProvider.options[':ROWKEY:,:ROWKEY:,:ROWKEY:']}</code><br>
109: * <code>#{...myDataProvider.options[':ROWID:,:ROWID:,:ROWID:']}</code><br>
110: * --> binds to an array of {@link Option} generated by scanning the rows of the
111: * TableDataProvider (without moving the cursor). If the base object is a
112: * DataProvider, but not a TableDataProvider, the resulting Option[] will have
113: * one element. Note that the special :ROWKEY: and :ROWID: field IDs can be
114: * used here.</p>
115: *
116: * <p>
117: * <code>#{...myDataProvider.stringList.FIELD_ID}</code><br>
118: * <code>#{...myDataProvider.stringList['FIELD_ID']}</code><br>
119: * <code>#{...myDataProvider.stringList[':ROWKEY:']}</code><br>
120: * <code>#{...myDataProvider.stringList[':ROWID:']}</code><br>
121: * --> binds to an array of String generated by scanning the rows of the
122: * TableDataProvider (without moving the cursor) and calling toString() on each
123: * value. If the base object is a DataProvider, but not a TableDataProvider,
124: * the resulting String[] will have one element. Note that the special :ROWKEY:
125: * and :ROWID: field IDs can be used here.</p>
126: *
127: * @author Joe Nuxoll
128: */
129: public class DataProviderPropertyResolver extends PropertyResolver {
130:
131: public static final String VALUE_KEY = "value"; // NOI18N
132: public static final String SELECT_ITEMS_KEY = "selectItems"; // NOI18N
133: public static final String OPTIONS_KEY = "options"; // NOI18N
134: public static final String STRING_LIST_KEY = "stringList"; // NOI18N
135: public static final String ROWID_FKEY = ":ROWID:"; // NOI18N
136: public static final String ROWKEY_FKEY = ":ROWKEY:"; // NOI18N
137:
138: /**
139: * storage for nested PropertyResolver (decorator pattern)
140: */
141: protected PropertyResolver nested;
142:
143: /**
144: * Constructs a DataProviderPropertyResolver using the specified
145: * PropertyResolver as the pass-thru PropertyResolver (decorator pattern)
146: *
147: * @param nested PropertyResolver
148: */
149: public DataProviderPropertyResolver(PropertyResolver nested) {
150: this .nested = nested;
151: }
152:
153: /**
154: * {@inheritDoc}
155: */
156: public Object getValue(Object base, Object property)
157: throws EvaluationException, PropertyNotFoundException {
158:
159: if (base instanceof DataProvider) {
160: DataProvider provider = (DataProvider) base;
161:
162: if (VALUE_KEY.equals(property)) {
163: return new ValueData(provider);
164:
165: } else if (SELECT_ITEMS_KEY.equals(property)) {
166: return new SelectItemsData(provider);
167:
168: } else if (OPTIONS_KEY.equals(property)) {
169: return new OptionsData(provider);
170:
171: } else if (STRING_LIST_KEY.equals(property)) {
172: return new StringListData(provider);
173: }
174:
175: } else if (base instanceof ValueData) {
176: return ((ValueData) base).getValue(property.toString());
177:
178: } else if (base instanceof SelectItemsData) {
179: return ((SelectItemsData) base).getSelectItems(property
180: .toString());
181: }
182:
183: if (nested != null) {
184: return nested.getValue(base, property);
185: }
186:
187: throw new PropertyNotFoundException("Property [" + property
188: + "] not found in object [" + base + "]");
189: }
190:
191: /**
192: * {@inheritDoc}
193: */
194: public Object getValue(Object base, int row)
195: throws EvaluationException, PropertyNotFoundException {
196:
197: return nested.getValue(base, row);
198: }
199:
200: /**
201: * {@inheritDoc}
202: */
203: public void setValue(Object base, Object property, Object value)
204: throws EvaluationException, PropertyNotFoundException {
205:
206: if (base instanceof ValueData) {
207: ((ValueData) base).setValue("" + property, value);
208:
209: } else if (nested != null) {
210: nested.setValue(base, property, value);
211:
212: } else {
213: throw new PropertyNotFoundException("Property [" + property
214: + "] not found in object [" + base + "]");
215: }
216: }
217:
218: /**
219: * {@inheritDoc}
220: */
221: public void setValue(Object base, int row, Object value)
222: throws EvaluationException, PropertyNotFoundException {
223:
224: nested.setValue(base, row, value);
225: }
226:
227: /**
228: * {@inheritDoc}
229: */
230: public boolean isReadOnly(Object base, Object property)
231: throws EvaluationException, PropertyNotFoundException {
232:
233: if (base instanceof ValueData) {
234: return ((ValueData) base).isReadOnly("" + property);
235: }
236:
237: if (base instanceof SelectItemsData) {
238: return true;
239: }
240:
241: if (base instanceof DataProvider) {
242: if (VALUE_KEY.equals(property)) {
243: return true;
244:
245: } else if (SELECT_ITEMS_KEY.equals(property)
246: || OPTIONS_KEY.equals(property)
247: || STRING_LIST_KEY.equals(property)) {
248: return true;
249: }
250:
251: }
252:
253: if (nested != null) {
254: return nested.isReadOnly(base, property);
255: }
256:
257: throw new PropertyNotFoundException("Property [" + property
258: + "] not found in object [" + base + "]");
259: }
260:
261: /**
262: * {@inheritDoc}
263: */
264: public boolean isReadOnly(Object base, int row)
265: throws EvaluationException, PropertyNotFoundException {
266:
267: return nested.isReadOnly(base, row);
268: }
269:
270: /**
271: * {@inheritDoc}
272: */
273: public Class getType(Object base, Object property)
274: throws EvaluationException, PropertyNotFoundException {
275:
276: if (base instanceof DataProvider) {
277: if (VALUE_KEY.equals(property)) {
278: return ValueData.class;
279:
280: } else if (SELECT_ITEMS_KEY.equals(property)) {
281: return SelectItemsData.class;
282:
283: } else if (OPTIONS_KEY.equals(property)) {
284: return OptionsData.class;
285:
286: } else if (STRING_LIST_KEY.equals(property)) {
287: return StringListData.class;
288: }
289:
290: } else if (base instanceof ValueData) {
291: return ((ValueData) base).getType("" + property);
292:
293: } else if (base instanceof SelectItemsData) {
294: return ArrayList.class;
295: }
296:
297: if (nested != null) {
298: return nested.getType(base, property);
299: }
300:
301: throw new PropertyNotFoundException("Property [" + property
302: + "] not found in object [" + base + "]");
303: }
304:
305: /**
306: * {@inheritDoc}
307: */
308: public Class getType(Object base, int row)
309: throws EvaluationException, PropertyNotFoundException {
310: return nested.getType(base, row);
311: }
312:
313: // --------------------------------------------------------------- ValueData
314:
315: /**
316: *
317: */
318: private class ValueData {
319:
320: /**
321: *
322: */
323: protected DataProvider provider;
324:
325: /**
326: *
327: */
328: public ValueData(DataProvider provider) {
329: this .provider = provider;
330: }
331:
332: /**
333: *
334: */
335: public Object getValue(String fieldId)
336: throws PropertyNotFoundException {
337:
338: if (fieldId == null) {
339: return null;
340: }
341:
342: Object value = null;
343:
344: if (ROWKEY_FKEY.equals(fieldId)) {
345: if (provider instanceof TableDataProvider) {
346: return ((TableDataProvider) provider)
347: .getCursorRow();
348: }
349: if (provider instanceof TableRowDataProvider) {
350: return ((TableRowDataProvider) provider)
351: .getTableRow();
352: }
353:
354: } else if (ROWID_FKEY.equals(fieldId)) {
355: if (provider instanceof TableDataProvider) {
356: return ((TableDataProvider) provider)
357: .getCursorRow().getRowId();
358: }
359: if (provider instanceof TableRowDataProvider) {
360: return ((TableRowDataProvider) provider)
361: .getTableRow().getRowId();
362: }
363:
364: } else {
365: try {
366: FieldKey fk = provider.getFieldKey(fieldId);
367: if (fk != null) {
368: // <RAVE> - 6334873 - No exception on empty TDP
369: // value = provider.getValue(fk);
370: try {
371: value = provider.getValue(fk);
372: } catch (IndexOutOfBoundsException e) {
373: value = null;
374: }
375: // </RAVE>
376: } else {
377: throw new PropertyNotFoundException("Field '"
378: + fieldId
379: + "' not found in DataProvider.");
380: }
381: } catch (Exception x) {
382: throw new PropertyNotFoundException(x);
383: }
384: }
385:
386: return value;
387: }
388:
389: /**
390: *
391: */
392: public void setValue(String fieldId, Object value)
393: throws PropertyNotFoundException {
394:
395: if (fieldId == null) {
396: return;
397: }
398:
399: if (ROWKEY_FKEY.equals(fieldId) && value instanceof RowKey) {
400: if (provider instanceof TableDataProvider) {
401: try {
402: ((TableDataProvider) provider)
403: .setCursorRow((RowKey) value);
404: return;
405: } catch (Exception x) {
406: x.printStackTrace();
407: }
408: }
409: if (provider instanceof TableRowDataProvider) {
410: try {
411: ((TableRowDataProvider) provider)
412: .setTableRow((RowKey) value);
413: return;
414: } catch (Exception x) {
415: x.printStackTrace();
416: }
417: }
418:
419: } else if (ROWID_FKEY.equals(fieldId)
420: && value instanceof String) {
421: if (provider instanceof TableDataProvider) {
422: try {
423: RowKey row = ((TableDataProvider) provider)
424: .getRowKey((String) value);
425: ((TableDataProvider) provider)
426: .setCursorRow(row);
427: return;
428: } catch (Exception x) {
429: x.printStackTrace();
430: }
431: }
432: if (provider instanceof TableRowDataProvider) {
433: try {
434: RowKey row = ((TableRowDataProvider) provider)
435: .getTableDataProvider().getRowKey(
436: (String) value);
437: ((TableRowDataProvider) provider)
438: .setTableRow(row);
439: return;
440: } catch (Exception x) {
441: x.printStackTrace();
442: }
443: }
444:
445: } else {
446: try {
447: FieldKey fk = provider.getFieldKey(fieldId);
448: if (fk != null) {
449: // <RAVE> - 6334873 - No exception on empty TDP
450: // provider.setValue(fk, value);
451: try {
452: provider.setValue(fk, value);
453: } catch (IndexOutOfBoundsException e) {
454: ; // Swallow and ignore
455: }
456: // </RAVE>
457: } else {
458: throw new PropertyNotFoundException("Field '"
459: + fieldId
460: + "' not found in DataProvider.");
461: }
462: } catch (Exception x) {
463: throw new PropertyNotFoundException(x);
464: }
465: }
466: }
467:
468: /**
469: *
470: */
471: public boolean isReadOnly(String fieldId)
472: throws PropertyNotFoundException {
473:
474: if (ROWKEY_FKEY.equals(fieldId)
475: || ROWID_FKEY.equals(fieldId)) {
476: return false;
477: }
478:
479: try {
480: FieldKey fk = provider.getFieldKey(fieldId);
481: if (fk != null) {
482: return provider.isReadOnly(fk);
483: } else {
484: throw new PropertyNotFoundException("Field '"
485: + fieldId + "' not found in DataProvider.");
486: }
487: } catch (Exception x) {
488: throw new PropertyNotFoundException(x);
489: }
490: }
491:
492: /**
493: *
494: */
495: public Class getType(String fieldId)
496: throws PropertyNotFoundException {
497:
498: if (ROWKEY_FKEY.equals(fieldId)) {
499: return RowKey.class;
500:
501: } else if (ROWID_FKEY.equals(fieldId)) {
502: return String.class;
503: }
504:
505: try {
506: FieldKey fk = provider.getFieldKey(fieldId);
507: if (fk != null) {
508: return provider.getType(fk);
509: } else {
510: throw new PropertyNotFoundException("Field '"
511: + fieldId + "' not found in DataProvider.");
512: }
513: } catch (Exception x) {
514: throw new PropertyNotFoundException(x);
515: }
516: }
517: }
518:
519: // --------------------------------------------------------- SelectItemsData
520:
521: /**
522: *
523: */
524: private class SelectItemsData {
525:
526: /**
527: *
528: */
529: protected DataProvider provider;
530:
531: /**
532: *
533: */
534: public SelectItemsData(DataProvider provider) {
535: this .provider = provider;
536: }
537:
538: /**
539: *
540: */
541: protected Object getValue(DataProvider provider,
542: String fieldId, RowKey row) {
543:
544: if (fieldId == null) {
545: return null;
546: }
547:
548: Object value = null;
549:
550: if (ROWKEY_FKEY.equals(fieldId)) {
551: value = row != null ? row
552: : (provider instanceof TableRowDataProvider ? ((TableRowDataProvider) provider)
553: .getTableRow()
554: : null);
555:
556: } else if (ROWID_FKEY.equals(fieldId)) {
557: value = row != null ? row.getRowId()
558: : (provider instanceof TableRowDataProvider ? ((TableRowDataProvider) provider)
559: .getTableRow().getRowId()
560: : null);
561:
562: } else {
563: try {
564: FieldKey fk = provider.getFieldKey(fieldId);
565: if (fk != null) {
566: if (row != null
567: && provider instanceof TableDataProvider) {
568: value = ((TableDataProvider) provider)
569: .getValue(fk, row);
570: } else {
571: value = provider.getValue(fk);
572: }
573: }
574: } catch (Exception x) {
575: // throw the puppy to help out the developer
576: // diagnose *his* application problem.
577: if (x instanceof RuntimeException) {
578: throw (RuntimeException) x;
579: } else {
580: // should never be here....
581: x.printStackTrace();
582: }
583: }
584: }
585:
586: return value;
587: }
588:
589: /**
590: *
591: */
592: protected Object getSelectItem(Object itemValue,
593: Object itemLabel, Object itemDescr) {
594:
595: if (itemValue != null && itemLabel != null
596: && itemDescr != null) {
597: return new SelectItem(itemValue, itemLabel.toString(),
598: itemDescr.toString());
599: }
600:
601: else if (itemValue != null && itemLabel != null) {
602: return new SelectItem(itemValue, itemLabel.toString());
603: }
604:
605: else if (itemValue != null) {
606: return new SelectItem(itemValue);
607: }
608:
609: return null;
610: }
611:
612: /**
613: *
614: */
615: public Object getSelectItems(String columns) {
616: /*
617: * returns a List of Objects or SelectItems
618: *
619: * (examples based on PERSON database table keys):
620: *
621: * "NAME" -->
622: * returns a List filled with SelectItem objects,
623: * with the 'itemValue' set to NAME's values
624: *
625: * "PERSONID,NAME" -->
626: * returns a List filled with SelectItem objects,
627: * with the 'itemValue' set to PERSONID's values,
628: * and the 'itemLabel' set to NAME's values
629: *
630: * "PERSONID,NAME,JOBTITLE" -->
631: * returns a List filled with SelectItem objects,
632: * with the 'itemValue' set to PERSONID's values,
633: * the 'itemLabel' set to NAME's values,
634: * and the 'itemDescription' set to JOBTITLE's values
635: *
636: * Any cases that are out-of-scope throw IllegalArgumentException
637: */
638: String valueId = null;
639: String labelId = null;
640: String descrId = null;
641:
642: List cols = new ArrayList();
643: String col;
644: boolean quoteOpen = false;
645: int currStart = 0;
646: for (int i = 0; i < columns.length(); i++) {
647: char c = columns.charAt(i);
648: if (c == '\'') {
649: quoteOpen = !quoteOpen;
650: } else if (c == ',' && !quoteOpen) {
651: col = columns.substring(currStart, i);
652: if (col.length() > 0) {
653: cols.add(col);
654: }
655: currStart = i + 1;
656: }
657: }
658:
659: //get the remaining stuff after the last period
660: if (currStart < columns.length()) {
661: col = columns.substring(currStart);
662: cols.add(col);
663: }
664:
665: String[] args = (String[]) cols.toArray(new String[cols
666: .size()]);
667: if (args.length < 1) {
668: throw new IllegalArgumentException();
669: }
670: valueId = args[0];
671: if (args.length > 1) {
672: labelId = args[1];
673: }
674: if (args.length > 2) {
675: descrId = args[2];
676: }
677:
678: ArrayList list = new ArrayList();
679:
680: if (provider instanceof TableDataProvider) {
681:
682: TableDataProvider tableProvider = (TableDataProvider) provider;
683: int rowCount = tableProvider.getRowCount();
684: if (rowCount < 0) {
685: rowCount = 999;
686: }
687:
688: RowKey[] rows = tableProvider
689: .getRowKeys(rowCount, null);
690:
691: for (int i = 0; i < rows.length; i++) {
692:
693: Object itemValue = getValue(provider, valueId,
694: rows[i]);
695: Object itemLabel = getValue(provider, labelId,
696: rows[i]);
697: Object itemDescr = getValue(provider, descrId,
698: rows[i]);
699:
700: Object selectItem = getSelectItem(itemValue,
701: itemLabel, itemDescr);
702: if (selectItem != null) {
703: list.add(selectItem);
704: }
705:
706: }
707:
708: } else {
709:
710: Object itemValue = getValue(provider, valueId, null);
711: Object itemLabel = getValue(provider, labelId, null);
712: Object itemDescr = getValue(provider, descrId, null);
713:
714: Object selectItem = getSelectItem(itemValue, itemLabel,
715: itemDescr);
716: if (selectItem != null) {
717: list.add(selectItem);
718: }
719:
720: }
721: return list;
722: }
723: }
724:
725: // ------------------------------------------------------------- OptionsData
726:
727: /**
728: *
729: */
730: private class OptionsData extends SelectItemsData {
731:
732: /**
733: *
734: */
735: public OptionsData(DataProvider provider) {
736: super (provider);
737: }
738:
739: /**
740: *
741: */
742: protected Object getSelectItem(Object itemValue,
743: Object itemLabel, Object itemDescr) {
744:
745: if (itemValue != null && itemLabel != null
746: && itemDescr != null) {
747: return new Option(itemValue, itemLabel.toString(),
748: itemDescr.toString());
749: }
750:
751: else if (itemValue != null && itemLabel != null) {
752: return new Option(itemValue, itemLabel.toString());
753: }
754:
755: else if (itemValue != null) {
756: return new Option(itemValue);
757: }
758:
759: return null;
760: }
761: }
762:
763: /**
764: *
765: */
766: private class StringListData extends SelectItemsData {
767:
768: /**
769: *
770: */
771: public StringListData(DataProvider provider) {
772: super (provider);
773: }
774:
775: /**
776: *
777: */
778: protected Object getSelectItem(Object itemValue,
779: Object itemLabel, Object itemDescr) {
780: if (itemValue != null) {
781: return new String(itemValue.toString());
782: }
783: return "";
784: }
785: }
786: }
|