View Javadoc
1   /*
2    * Prometheus: Application Framework
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.prometheus.data;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.oceanus.convert.OceanusDataConverter;
21  import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
22  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
23  import io.github.tonywasher.joceanus.metis.data.MetisDataEditState;
24  import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
25  import io.github.tonywasher.joceanus.metis.data.MetisDataResource;
26  import io.github.tonywasher.joceanus.metis.data.MetisDataState;
27  import io.github.tonywasher.joceanus.metis.field.MetisFieldSet;
28  import io.github.tonywasher.joceanus.metis.field.MetisFieldState;
29  import io.github.tonywasher.joceanus.metis.field.MetisFieldVersionValues;
30  import io.github.tonywasher.joceanus.metis.field.MetisFieldVersionedItem;
31  import io.github.tonywasher.joceanus.metis.list.MetisListKey;
32  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataList.PrometheusListStyle;
33  import io.github.tonywasher.joceanus.prometheus.exc.PrometheusDataException;
34  
35  import java.util.Iterator;
36  
37  /**
38   * Provides the abstract DataItem class as the basis for data items. The implementation of the
39   * interface means that this object can only be held in one list at a time and is unique within that
40   * list
41   *
42   * @see PrometheusDataList
43   */
44  public abstract class PrometheusDataItem
45          extends MetisFieldVersionedItem
46          implements PrometheusTableItem, Comparable<Object> {
47      /**
48       * Report fields.
49       */
50      private static final MetisFieldSet<PrometheusDataItem> FIELD_DEFS = MetisFieldSet.newFieldSet(PrometheusDataItem.class);
51  
52      /*
53       * FieldIds.
54       */
55      static {
56          FIELD_DEFS.declareLocalField(PrometheusDataResource.DATALIST_NAME, PrometheusDataItem::getList);
57          FIELD_DEFS.declareLocalField(PrometheusDataResource.DATAITEM_BASE, PrometheusDataItem::getBase);
58          FIELD_DEFS.declareLocalField(PrometheusDataResource.DATAITEM_TOUCH, PrometheusDataItem::getTouchStatus);
59          FIELD_DEFS.declareLocalField(PrometheusDataResource.DATAITEM_HEADER, PrometheusDataItem::isHeader);
60      }
61  
62      /**
63       * Validation error.
64       */
65      public static final String ERROR_VALIDATION = PrometheusDataResource.DATAITEM_ERROR_VALIDATION.getValue();
66  
67      /**
68       * Resolution error.
69       */
70      public static final String ERROR_RESOLUTION = PrometheusDataResource.DATAITEM_ERROR_RESOLUTION.getValue();
71  
72      /**
73       * Duplicate Id error.
74       */
75      public static final String ERROR_DUPLICATE = PrometheusDataResource.DATAITEM_ERROR_DUPLICATE.getValue();
76  
77      /**
78       * Unknown Id error.
79       */
80      public static final String ERROR_UNKNOWN = PrometheusDataResource.DATAITEM_ERROR_UNKNOWN.getValue();
81  
82      /**
83       * Existing value error.
84       */
85      public static final String ERROR_EXIST = PrometheusDataResource.DATAITEM_ERROR_EXIST.getValue();
86  
87      /**
88       * Missing value error.
89       */
90      public static final String ERROR_MISSING = PrometheusDataResource.DATAITEM_ERROR_MISSING.getValue();
91  
92      /**
93       * Value too long error.
94       */
95      public static final String ERROR_LENGTH = PrometheusDataResource.DATAITEM_ERROR_LENGTH.getValue();
96  
97      /**
98       * Value negative error.
99       */
100     public static final String ERROR_NEGATIVE = PrometheusDataResource.DATAITEM_ERROR_NEGATIVE.getValue();
101 
102     /**
103      * Value positive error.
104      */
105     public static final String ERROR_POSITIVE = PrometheusDataResource.DATAITEM_ERROR_POSITIVE.getValue();
106 
107     /**
108      * Value zero error.
109      */
110     public static final String ERROR_ZERO = PrometheusDataResource.DATAITEM_ERROR_ZERO.getValue();
111 
112     /**
113      * Value outside valid range.
114      */
115     public static final String ERROR_RANGE = PrometheusDataResource.DATAITEM_ERROR_RANGE.getValue();
116 
117     /**
118      * Value disabled error.
119      */
120     public static final String ERROR_DISABLED = PrometheusDataResource.DATAITEM_ERROR_DISABLED.getValue();
121 
122     /**
123      * Creation failure.
124      */
125     public static final String ERROR_CREATEITEM = PrometheusDataResource.DATAITEM_ERROR_CREATE.getValue();
126 
127     /**
128      * Multiple instances Error.
129      */
130     public static final String ERROR_MULT = PrometheusDataResource.DATAITEM_ERROR_MULTIPLE.getValue();
131 
132     /**
133      * Reserved name error.
134      */
135     public static final String ERROR_INVALIDCHAR = PrometheusDataResource.DATAITEM_ERROR_INVALIDCHAR.getValue();
136 
137     /**
138      * Standard Name length.
139      */
140     public static final int NAMELEN = 30;
141 
142     /**
143      * Standard Description length.
144      */
145     public static final int DESCLEN = 50;
146 
147     /**
148      * The list to which this item belongs.
149      */
150     private PrometheusDataList<?> theList;
151 
152     /**
153      * The item that this DataItem is based upon.
154      */
155     private PrometheusDataItem theBase;
156 
157     /**
158      * Is the item a header.
159      */
160     private boolean isHeader;
161 
162     /**
163      * Status.
164      */
165     private final PrometheusDataTouch theTouchStatus;
166 
167     /**
168      * Construct a new item.
169      *
170      * @param pList the list that this item is associated with
171      * @param uId   the Id of the new item (or 0 if not yet known)
172      */
173     protected PrometheusDataItem(final PrometheusDataList<?> pList,
174                                  final Integer uId) {
175         /* Record list and item references */
176         setIndexedId(uId);
177         theList = pList;
178 
179         /* Allocate id */
180         pList.setNewId(this);
181 
182         /* Create the touch status */
183         theTouchStatus = new PrometheusDataTouch();
184     }
185 
186     /**
187      * Construct a new item.
188      *
189      * @param pList   the list that this item is associated with
190      * @param pValues the data values
191      */
192     protected PrometheusDataItem(final PrometheusDataList<?> pList,
193                                  final PrometheusDataValues pValues) {
194         /* Record list and item references */
195         this(pList, pValues.getValue(MetisDataResource.DATA_ID, Integer.class));
196     }
197 
198     /**
199      * Construct a new item based on an old item.
200      *
201      * @param pList the list that this item is associated with
202      * @param pBase the old item
203      */
204     protected PrometheusDataItem(final PrometheusDataList<?> pList,
205                                  final PrometheusDataItem pBase) {
206         /* Initialise using standard constructor */
207         this(pList, pBase.getIndexedId());
208 
209         /* Initialise the valueSet */
210         getValues().copyFrom(pBase.getValues());
211 
212         /* Access the varying styles and the source state */
213         final PrometheusListStyle myStyle = pList.getStyle();
214         final PrometheusListStyle myBaseStyle = pBase.getList().getStyle();
215         final MetisDataState myState = pBase.getState();
216 
217         /* Switch on the styles */
218         switch (myStyle) {
219             /* We are building an update list (from Core) */
220             case UPDATE:
221                 switch (myState) {
222                     /* NEW/DELNEW need to be at version 1 */
223                     case DELNEW:
224                     case NEW:
225                         getValues().setVersion(1);
226                         break;
227 
228                     case DELETED:
229                         getValuesHistory().pushHistory(1);
230                         break;
231 
232                     /*
233                      * Changed items need to have new values at version 1 and originals at version 0
234                      */
235                     case CHANGED:
236                         setHistory(pBase);
237                         break;
238 
239                     /* No change for other states */
240                     default:
241                         break;
242                 }
243 
244                 /* Record the base item */
245                 theBase = pBase;
246                 break;
247 
248             /* We are building an edit item (from Core/Edit) */
249             case EDIT:
250                 /* Switch on the base style */
251                 switch (myBaseStyle) {
252                     /* New item from core we need to link back and copy flags */
253                     case CORE:
254                         theBase = pBase;
255                         copyFlags(pBase);
256                         break;
257                     /* Duplication in edit */
258                     case EDIT:
259                         /* set as a new item */
260                         getValues().setVersion(pList.getVersion() + 1);
261 
262                         /* Reset the Id */
263                         setIndexedId(0);
264                         pList.setNewId(this);
265                         break;
266                     default:
267                         break;
268                 }
269                 break;
270 
271             /* We are building a CORE item */
272             case CORE:
273                 /* set as a new item */
274                 getValues().setVersion(pList.getVersion() + 1);
275 
276                 /* If we are adding from Edit */
277                 if (myBaseStyle == PrometheusListStyle.EDIT) {
278                     /* Reset the Id */
279                     setIndexedId(0);
280                     pList.setNewId(this);
281                 }
282                 break;
283 
284             /* Creation of copy element not allowed */
285             case COPY:
286                 throw new IllegalArgumentException("Illegal creation of COPY element");
287 
288                 /* Nothing special for other styles */
289             case CLONE:
290             case DIFFER:
291             default:
292                 break;
293         }
294     }
295 
296     /**
297      * Obtain valueSet version.
298      *
299      * @return the valueSet version
300      */
301     public int getValueSetVersion() {
302         return getValues().getVersion();
303     }
304 
305     @Override
306     public String formatObject(final OceanusDataFormatter pFormatter) {
307         return this.getDataFieldSet().getName();
308     }
309 
310     /**
311      * Obtain the list.
312      *
313      * @return the list
314      */
315     public PrometheusDataList<?> getList() {
316         return theList;
317     }
318 
319     /**
320      * Obtain the dataSet.
321      *
322      * @return the dataSet
323      */
324     public PrometheusDataSet getDataSet() {
325         return getTheDataSet();
326     }
327 
328     /**
329      * Obtain the dataSet.
330      *
331      * @return the dataSet
332      */
333     private PrometheusDataSet getTheDataSet() {
334         return theList.getDataSet();
335     }
336 
337     /**
338      * Get the list style for this item.
339      *
340      * @return the list style
341      */
342     public PrometheusListStyle getStyle() {
343         return theList.getStyle();
344     }
345 
346     @Override
347     public MetisListKey getItemType() {
348         return theList.getItemType();
349     }
350 
351     @Override
352     public boolean isActive() {
353         return theTouchStatus.isActive();
354     }
355 
356     /**
357      * Is the item disabled?
358      *
359      * @return true/false
360      */
361     public boolean isDisabled() {
362         return false;
363     }
364 
365     /**
366      * Obtain the touchStatus.
367      *
368      * @return the touch status
369      */
370     public PrometheusDataTouch getTouchStatus() {
371         return theTouchStatus;
372     }
373 
374     @Override
375     public boolean isEditable() {
376         return !isDeleted();
377     }
378 
379     @Override
380     public boolean isHeader() {
381         return isHeader;
382     }
383 
384     /**
385      * Set the header indication.
386      *
387      * @param pHeader true/false
388      */
389     protected void setHeader(final boolean pHeader) {
390         isHeader = pHeader;
391     }
392 
393     /**
394      * Determine whether the item is locked (overridden if required).
395      *
396      * @return <code>true/false</code>
397      */
398     public boolean isLocked() {
399         return false;
400     }
401 
402     /**
403      * Determine whether the list is locked (overridden if required).
404      *
405      * @return <code>true/false</code>
406      */
407     public boolean isListLocked() {
408         return false;
409     }
410 
411     /**
412      * DeRegister any infoSet links.
413      */
414     public void deRegister() {
415     }
416 
417     /**
418      * Clear the touch status flag.
419      */
420     public void clearActive() {
421         theTouchStatus.resetTouches();
422     }
423 
424     /**
425      * Clear the item touches.
426      *
427      * @param pItemType the item type
428      */
429     public void clearTouches(final MetisListKey pItemType) {
430         theTouchStatus.resetTouches(pItemType);
431     }
432 
433     /**
434      * Touch the item.
435      *
436      * @param pObject object that references the item
437      */
438     public void touchItem(final PrometheusDataItem pObject) {
439         theTouchStatus.touchItem(pObject.getItemType());
440     }
441 
442     /**
443      * Touch underlying items that are referenced by this item.
444      */
445     public void touchUnderlyingItems() {
446     }
447 
448     /**
449      * Adjust touches on update.
450      */
451     public void touchOnUpdate() {
452     }
453 
454     /**
455      * Adjust map for this item.
456      */
457     public void adjustMapForItem() {
458     }
459 
460     /**
461      * update Maps.
462      */
463     public void updateMaps() {
464         /* Clear active flag and touch underlying items */
465         clearActive();
466         touchUnderlyingItems();
467 
468         /* Adjust the map for this item */
469         adjustMapForItem();
470     }
471 
472     /**
473      * Get the base item for this item.
474      *
475      * @return the Base item or <code>null</code>
476      */
477     public PrometheusDataItem getBase() {
478         return theBase;
479     }
480 
481     /**
482      * Set the base item for this item.
483      *
484      * @param pBase the Base item
485      */
486     public void setBase(final PrometheusDataItem pBase) {
487         theBase = pBase;
488     }
489 
490     /**
491      * Unlink the item from the list.
492      */
493     public void unLink() {
494         theList.remove(this);
495     }
496 
497     /**
498      * Set new version.
499      */
500     public void setNewVersion() {
501         getValues().setVersion(getNextVersion());
502     }
503 
504     @Override
505     public int getNextVersion() {
506         return theList.getVersion() + 1;
507     }
508 
509     @Override
510     public void popHistory() {
511         rewindToVersion(theList.getVersion());
512     }
513 
514     @Override
515     public void rewindToVersion(final int pVersion) {
516         /* If the item was newly created */
517         if (getOriginalValues().getVersion() > pVersion) {
518             /* Remove from list */
519             unLink();
520             deRegister();
521 
522             /* Return */
523             return;
524         }
525 
526         /* Loop while version is too high */
527         while (getValues().getVersion() > pVersion) {
528             /* Pop history */
529             getValuesHistory().popTheHistory();
530         }
531     }
532 
533     /**
534      * Set Change history for an update list so that the first and only entry in the change list is
535      * the original values of the base.
536      *
537      * @param pBase the base item
538      */
539     public final void setHistory(final PrometheusDataItem pBase) {
540         getValuesHistory().setHistory(pBase.getOriginalValues());
541     }
542 
543     @Override
544     public MetisDataDifference fieldChanged(final MetisFieldDef pField) {
545         return pField instanceof MetisFieldVersionedDef
546                 ? getValuesHistory().fieldChanged(pField)
547                 : MetisDataDifference.IDENTICAL;
548     }
549 
550     /**
551      * Note that this item has been validated.
552      */
553     public void setValidEdit() {
554         final MetisDataState myState = getState();
555         if (myState == MetisDataState.CLEAN) {
556             setEditState(MetisDataEditState.CLEAN);
557         } else if (theList.getStyle() == PrometheusListStyle.CORE) {
558             setEditState(MetisDataEditState.DIRTY);
559         } else {
560             setEditState(MetisDataEditState.VALID);
561         }
562     }
563 
564     @Override
565     public void addError(final String pError,
566                          final MetisDataFieldId pField) {
567         /* Set edit state and add the error */
568         super.addError(pError, pField);
569 
570         /* Note that the list has errors */
571         theList.setEditState(MetisDataEditState.ERROR);
572     }
573 
574     /**
575      * Copy flags.
576      *
577      * @param pItem the original item
578      */
579     private void copyFlags(final PrometheusDataItem pItem) {
580         theTouchStatus.copyMap(pItem.theTouchStatus);
581     }
582 
583     /**
584      * Resolve all references to current dataSet.
585      *
586      * @throws OceanusException on error
587      */
588     public void resolveDataSetLinks() throws OceanusException {
589     }
590 
591     /**
592      * Resolve a data link into a list.
593      *
594      * @param pFieldId the fieldId to resolve
595      * @param pList    the list to resolve against
596      * @throws OceanusException on error
597      */
598     protected void resolveDataLink(final MetisDataFieldId pFieldId,
599                                    final PrometheusDataList<?> pList) throws OceanusException {
600         /* Access the values */
601         final MetisFieldVersionValues myValues = getValues();
602 
603         /* Access value for field */
604         Object myValue = myValues.getValue(pFieldId);
605 
606         /* Convert dataItem reference to Id */
607         if (myValue instanceof PrometheusDataItem myItem) {
608             myValue = myItem.getIndexedId();
609         }
610 
611         /* Lookup Id reference */
612         if (myValue instanceof Integer i) {
613             final PrometheusDataItem myItem = pList.findItemById(i);
614             if (myItem == null) {
615                 addError(ERROR_UNKNOWN, pFieldId);
616                 throw new PrometheusDataException(this, ERROR_RESOLUTION);
617             }
618             myValues.setValue(pFieldId, myItem);
619 
620             /* Lookup Name reference */
621         } else if (myValue instanceof String s) {
622             final PrometheusDataItem myItem = pList.findItemByName(s);
623             if (myItem == null) {
624                 addError(ERROR_UNKNOWN, pFieldId);
625                 throw new PrometheusDataException(this, ERROR_RESOLUTION);
626             }
627             myValues.setValue(pFieldId, myItem);
628         }
629     }
630 
631     /**
632      * Is the item to be included in output XML?
633      *
634      * @param pField the field to check
635      * @return true/false
636      */
637     public boolean includeXmlField(final MetisDataFieldId pField) {
638         return false;
639     }
640 
641     @Override
642     public boolean equals(final Object pThat) {
643         /* Handle the trivial cases */
644         if (this == pThat) {
645             return true;
646         }
647         if (pThat == null) {
648             return false;
649         }
650 
651         /* Make sure that the object is the same class */
652         if (pThat.getClass() != getClass()) {
653             return false;
654         }
655 
656         /* Access the object as a DataItem */
657         final PrometheusDataItem myItem = (PrometheusDataItem) pThat;
658 
659         /* Check the id */
660         if (compareId(myItem) != 0) {
661             return false;
662         }
663 
664         /* Loop through the fields */
665         final Iterator<MetisFieldDef> myIterator = getDataFieldSet().fieldIterator();
666         while (myIterator.hasNext()) {
667             /* Access Field */
668             final MetisFieldDef myField = myIterator.next();
669 
670             /* Skip if not used in equality */
671             if (!(myField instanceof MetisFieldVersionedDef myVersioned)
672                     || !myVersioned.isEquality()) {
673                 continue;
674             }
675 
676             /* Access the values */
677             final Object myValue = myField.getFieldValue(this);
678             final Object myNew = myField.getFieldValue(myItem);
679 
680             /* Check the field */
681             if (!MetisDataDifference.isEqual(myValue, myNew)) {
682                 return false;
683             }
684         }
685 
686         /* Return identical */
687         return true;
688     }
689 
690     @Override
691     public int hashCode() {
692         /* hash code is Id for simplicity */
693         return getIndexedId();
694     }
695 
696     @Override
697     public int compareTo(final Object pThat) {
698         /* Handle the trivial cases */
699         if (this.equals(pThat)) {
700             return 0;
701         }
702         if (pThat == null) {
703             return -1;
704         }
705 
706         /* Non-DataItems are last */
707         if (!(pThat instanceof PrometheusDataItem)) {
708             return -1;
709         }
710 
711         /* Check data type */
712         final PrometheusDataItem myThat = (PrometheusDataItem) pThat;
713         int iDiff = getItemType().getItemKey() - myThat.getItemType().getItemKey();
714         if (iDiff != 0) {
715             return iDiff;
716         }
717 
718         /* Check values and finally id */
719         iDiff = compareValues(myThat);
720         return iDiff != 0 ? iDiff : compareId(myThat);
721     }
722 
723     /**
724      * compareTo another dataItem.
725      *
726      * @param pThat the DataItem to compare
727      * @return the order
728      */
729     protected abstract int compareValues(PrometheusDataItem pThat);
730 
731     /**
732      * compareTo another dataItem.
733      *
734      * @param pThat the DataItem to compare
735      * @return the order
736      */
737     protected int compareId(final PrometheusDataItem pThat) {
738         return getIndexedId() - pThat.getIndexedId();
739     }
740 
741     /**
742      * Get the state of the underlying record.
743      *
744      * @return the underlying state
745      */
746     protected MetisDataState getBaseState() {
747         final PrometheusDataItem myBase = getBase();
748         return (myBase == null)
749                 ? MetisDataState.NOSTATE
750                 : myBase.getState();
751     }
752 
753     /**
754      * Determine index of element within the list.
755      *
756      * @return The index
757      */
758     public int indexOf() {
759         /* Return index */
760         return theList.indexOf(this);
761     }
762 
763     /**
764      * Apply changes to the item from a changed version. Overwritten by objects that have changes
765      *
766      * @param pElement the changed element.
767      * @return were changes made?
768      */
769     public boolean applyChanges(final PrometheusDataItem pElement) {
770         return false;
771     }
772 
773     /**
774      * Validate the element
775      * <p>
776      * Dirty items become valid.
777      */
778     public void validate() {
779         getList().getValidator().validate(this);
780     }
781 
782     /**
783      * Does the string contain only valid characters (no control chars)?
784      *
785      * @param pString     the string
786      * @param pDisallowed the set of additional disallowed characters
787      * @return true/false
788      */
789     public static boolean validString(final String pString,
790                                       final String pDisallowed) {
791         /* Loop through the string */
792         for (int i = 0; i < pString.length(); i++) {
793             final int myChar = pString.codePointAt(i);
794             /* Check for ISO control */
795             if (Character.isISOControl(myChar)) {
796                 return false;
797             }
798 
799             /* Check for disallowed value */
800             if (pDisallowed != null
801                     && pDisallowed.indexOf(myChar) != -1) {
802                 return false;
803             }
804         }
805         return true;
806     }
807 
808     /**
809      * Obtain the byte length of a string.
810      *
811      * @param pString the string
812      * @return the length
813      */
814     public static int byteLength(final String pString) {
815         return OceanusDataConverter.stringToByteArray(pString).length;
816     }
817 
818     /**
819      * Remove the item (and any subitems from the lists).
820      */
821     public void removeItem() {
822         theList.remove(this);
823     }
824 
825     /**
826      * Obtain the field state.
827      *
828      * @param pField the field
829      * @return the state
830      */
831     public MetisFieldState getFieldState(final MetisDataFieldId pField) {
832         /* Determine DELETED state */
833         if (isDeleted()) {
834             return MetisFieldState.DELETED;
835 
836             /* Determine Error state */
837         } else if (hasErrors() && hasErrors(pField)) {
838             return MetisFieldState.ERROR;
839 
840             /* Determine Changed state */
841         } else if (fieldChanged(pField).isDifferent()) {
842             return MetisFieldState.CHANGED;
843 
844             /* Determine standard states */
845         } else {
846             switch (getState()) {
847                 case NEW:
848                     return MetisFieldState.NEW;
849                 case RECOVERED:
850                     return MetisFieldState.RESTORED;
851                 default:
852                     return MetisFieldState.NORMAL;
853             }
854         }
855     }
856 
857     /**
858      * Obtain the item State.
859      *
860      * @return the state
861      */
862     public MetisFieldState getItemState() {
863         /* Determine DELETED state */
864         if (isDeleted()) {
865             return MetisFieldState.DELETED;
866 
867             /* Determine Error state */
868         } else if (hasErrors()) {
869             return MetisFieldState.ERROR;
870 
871             /* Determine Changed state */
872         } else if (hasHistory()) {
873             return MetisFieldState.CHANGED;
874 
875             /* Determine standard states */
876         } else {
877             switch (getState()) {
878                 case NEW:
879                     return MetisFieldState.NEW;
880                 case RECOVERED:
881                     return MetisFieldState.RESTORED;
882                 default:
883                     return MetisFieldState.NORMAL;
884             }
885         }
886     }
887 }