View Javadoc
1   /*
2    * MoneyWise: Finance Application
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.moneywise.data.basic;
18  
19  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
20  import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
21  import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataNamedItem;
22  import io.github.tonywasher.joceanus.metis.field.MetisFieldSet;
23  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataValidator.MoneyWiseDataValidatorParentDefaults;
24  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseCategoryInterface;
25  import io.github.tonywasher.joceanus.moneywise.exc.MoneyWiseDataException;
26  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
27  import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
28  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataInstanceMap;
29  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
30  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataResource;
31  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataSet;
32  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataValues;
33  import io.github.tonywasher.joceanus.prometheus.data.PrometheusEncryptedDataItem;
34  import io.github.tonywasher.joceanus.prometheus.data.PrometheusEncryptedFieldSet;
35  import io.github.tonywasher.joceanus.prometheus.data.PrometheusEncryptedPair;
36  import io.github.tonywasher.joceanus.prometheus.data.PrometheusListStyle;
37  import io.github.tonywasher.joceanus.prometheus.data.PrometheusStaticDataItem;
38  
39  import java.util.Iterator;
40  
41  /**
42   * Category Base class.
43   */
44  public abstract class MoneyWiseCategoryBase
45          extends PrometheusEncryptedDataItem
46          implements MetisDataNamedItem {
47      /**
48       * Validator for categories.
49       *
50       * @param <T> the item type
51       */
52      public interface MoneyWiseDataValidatorCategory<T extends MoneyWiseCategoryBase>
53              extends MoneyWiseDataValidatorParentDefaults<T> {
54      }
55  
56      /**
57       * Separator.
58       */
59      public static final String STR_SEP = ":";
60  
61      /**
62       * Local Report fields.
63       */
64      private static final PrometheusEncryptedFieldSet<MoneyWiseCategoryBase> FIELD_DEFS = PrometheusEncryptedFieldSet.newEncryptedFieldSet(MoneyWiseCategoryBase.class);
65  
66      /*
67       * FieldIds.
68       */
69      static {
70          FIELD_DEFS.declareEncryptedStringField(PrometheusDataResource.DATAITEM_FIELD_NAME, NAMELEN);
71          FIELD_DEFS.declareEncryptedStringField(PrometheusDataResource.DATAITEM_FIELD_DESC, DESCLEN);
72          FIELD_DEFS.declareLinkField(PrometheusDataResource.DATAGROUP_PARENT);
73          FIELD_DEFS.declareDerivedVersionedField(MoneyWiseBasicResource.CATEGORY_SUBCAT);
74      }
75  
76      /**
77       * Copy Constructor.
78       *
79       * @param pList     the list
80       * @param pCategory The Category to copy
81       */
82      protected MoneyWiseCategoryBase(final MoneyWiseCategoryBaseList<?> pList,
83                                      final MoneyWiseCategoryBase pCategory) {
84          /* Set standard values */
85          super(pList, pCategory);
86      }
87  
88      /**
89       * Values constructor.
90       *
91       * @param pList   the List to add to
92       * @param pValues the values constructor
93       * @throws OceanusException on error
94       */
95      protected MoneyWiseCategoryBase(final MoneyWiseCategoryBaseList<?> pList,
96                                      final PrometheusDataValues pValues) throws OceanusException {
97          /* Initialise the item */
98          super(pList, pValues);
99  
100         /* Protect against exceptions */
101         try {
102             /* Store the Name */
103             Object myValue = pValues.getValue(PrometheusDataResource.DATAITEM_FIELD_NAME);
104             if (myValue instanceof String s) {
105                 setValueName(s);
106             } else if (myValue instanceof byte[] ba) {
107                 setValueName(ba);
108             }
109 
110             /* Store the Description */
111             myValue = pValues.getValue(PrometheusDataResource.DATAITEM_FIELD_DESC);
112             if (myValue instanceof String s) {
113                 setValueDesc(s);
114             } else if (myValue instanceof byte[] ba) {
115                 setValueDesc(ba);
116             }
117 
118             /* Store the Parent */
119             myValue = pValues.getValue(PrometheusDataResource.DATAGROUP_PARENT);
120             if (myValue instanceof Integer i) {
121                 setValueParent(i);
122             } else if (myValue instanceof String s) {
123                 setValueParent(s);
124             }
125 
126             /* Resolve the subCategory */
127             resolveSubCategory();
128 
129             /* Catch Exceptions */
130         } catch (OceanusException e) {
131             /* Pass on exception */
132             throw new MoneyWiseDataException(this, ERROR_CREATEITEM, e);
133         }
134     }
135 
136     /**
137      * Edit Constructor.
138      *
139      * @param pList the list
140      */
141     protected MoneyWiseCategoryBase(final MoneyWiseCategoryBaseList<?> pList) {
142         super(pList, 0);
143         setNextDataKeySet();
144     }
145 
146     @Override
147     public String formatObject(final OceanusDataFormatter pFormatter) {
148         return toString();
149     }
150 
151     @Override
152     public String toString() {
153         return getName();
154     }
155 
156     @Override
157     public boolean includeXmlField(final MetisDataFieldId pField) {
158         /* Determine whether fields should be included */
159         if (PrometheusDataResource.DATAITEM_FIELD_NAME.equals(pField)) {
160             return true;
161         }
162         if (PrometheusDataResource.DATAITEM_FIELD_DESC.equals(pField)) {
163             return getDesc() != null;
164         }
165         if (PrometheusDataResource.DATAGROUP_PARENT.equals(pField)) {
166             return getParentCategory() != null;
167         }
168 
169         /* Pass call on */
170         return super.includeXmlField(pField);
171     }
172 
173     @Override
174     public String getName() {
175         return getValues().getValue(PrometheusDataResource.DATAITEM_FIELD_NAME, String.class);
176     }
177 
178     /**
179      * Obtain Encrypted name.
180      *
181      * @return the bytes
182      */
183     public byte[] getNameBytes() {
184         return getValues().getEncryptedBytes(PrometheusDataResource.DATAITEM_FIELD_NAME);
185     }
186 
187     /**
188      * Obtain Encrypted Name Field.
189      *
190      * @return the Field
191      */
192     private PrometheusEncryptedPair getNameField() {
193         return getValues().getEncryptedPair(PrometheusDataResource.DATAITEM_FIELD_NAME);
194     }
195 
196     /**
197      * Obtain Description.
198      *
199      * @return the description
200      */
201     public String getDesc() {
202         return getValues().getValue(PrometheusDataResource.DATAITEM_FIELD_DESC, String.class);
203     }
204 
205     /**
206      * Obtain Encrypted description.
207      *
208      * @return the bytes
209      */
210     public byte[] getDescBytes() {
211         return getValues().getEncryptedBytes(PrometheusDataResource.DATAITEM_FIELD_DESC);
212     }
213 
214     /**
215      * Obtain Encrypted Description Field.
216      *
217      * @return the Field
218      */
219     private PrometheusEncryptedPair getDescField() {
220         return getValues().getEncryptedPair(PrometheusDataResource.DATAITEM_FIELD_DESC);
221     }
222 
223     /**
224      * Obtain Category Type.
225      *
226      * @return the type
227      */
228     public abstract PrometheusStaticDataItem getCategoryType();
229 
230     /**
231      * Obtain categoryTypeId.
232      *
233      * @return the categoryTypeId
234      */
235     public Integer getCategoryTypeId() {
236         final PrometheusStaticDataItem myType = getCategoryType();
237         return (myType == null)
238                 ? null
239                 : myType.getIndexedId();
240     }
241 
242     /**
243      * Obtain CategoryTypeName.
244      *
245      * @return the categoryTypeName
246      */
247     public String getCategoryTypeName() {
248         final PrometheusStaticDataItem myType = getCategoryType();
249         return myType == null
250                 ? null
251                 : myType.getName();
252     }
253 
254     /**
255      * Obtain CategoryTypeClass.
256      *
257      * @return the categoryTypeClass
258      */
259     public abstract MoneyWiseCategoryInterface getCategoryTypeClass();
260 
261     /**
262      * Obtain Cash Category Parent.
263      *
264      * @return the parent
265      */
266     public abstract MoneyWiseCategoryBase getParentCategory();
267 
268     /**
269      * Obtain parentId.
270      *
271      * @return the parentId
272      */
273     public Integer getParentCategoryId() {
274         final MoneyWiseCategoryBase myParent = getParentCategory();
275         return myParent == null
276                 ? null
277                 : myParent.getIndexedId();
278     }
279 
280     /**
281      * Obtain parentName.
282      *
283      * @return the parentName
284      */
285     public String getParentCategoryName() {
286         final MoneyWiseCategoryBase myParent = getParentCategory();
287         return myParent == null
288                 ? null
289                 : myParent.getName();
290     }
291 
292     /**
293      * Obtain subCategory.
294      *
295      * @return the subCategory
296      */
297     public String getSubCategory() {
298         return getValues().getValue(MoneyWiseBasicResource.CATEGORY_SUBCAT, String.class);
299     }
300 
301     /**
302      * Set name value.
303      *
304      * @param pValue the value
305      * @throws OceanusException on error
306      */
307     private void setValueName(final String pValue) throws OceanusException {
308         setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_NAME, pValue);
309     }
310 
311     /**
312      * Set name value.
313      *
314      * @param pBytes the value
315      * @throws OceanusException on error
316      */
317     private void setValueName(final byte[] pBytes) throws OceanusException {
318         setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_NAME, pBytes, String.class);
319     }
320 
321     /**
322      * Set name value.
323      *
324      * @param pValue the value
325      */
326     private void setValueName(final PrometheusEncryptedPair pValue) {
327         getValues().setUncheckedValue(PrometheusDataResource.DATAITEM_FIELD_NAME, pValue);
328     }
329 
330     /**
331      * Set description value.
332      *
333      * @param pValue the value
334      * @throws OceanusException on error
335      */
336     private void setValueDesc(final String pValue) throws OceanusException {
337         setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_DESC, pValue);
338     }
339 
340     /**
341      * Set description value.
342      *
343      * @param pBytes the value
344      * @throws OceanusException on error
345      */
346     private void setValueDesc(final byte[] pBytes) throws OceanusException {
347         setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_DESC, pBytes, String.class);
348     }
349 
350     /**
351      * Set description value.
352      *
353      * @param pValue the value
354      */
355     private void setValueDesc(final PrometheusEncryptedPair pValue) {
356         getValues().setUncheckedValue(PrometheusDataResource.DATAITEM_FIELD_DESC, pValue);
357     }
358 
359     /**
360      * Set parent value.
361      *
362      * @param pValue the value
363      */
364     private void setValueParent(final MoneyWiseCategoryBase pValue) {
365         getValues().setUncheckedValue(PrometheusDataResource.DATAGROUP_PARENT, pValue);
366     }
367 
368     /**
369      * Set parent id.
370      *
371      * @param pValue the value
372      */
373     private void setValueParent(final Integer pValue) {
374         getValues().setUncheckedValue(PrometheusDataResource.DATAGROUP_PARENT, pValue);
375     }
376 
377     /**
378      * Set parent name.
379      *
380      * @param pValue the value
381      */
382     private void setValueParent(final String pValue) {
383         getValues().setUncheckedValue(PrometheusDataResource.DATAGROUP_PARENT, pValue);
384     }
385 
386     /**
387      * Set subCategory name.
388      *
389      * @param pValue the value
390      */
391     private void setValueSubCategory(final String pValue) {
392         getValues().setUncheckedValue(MoneyWiseBasicResource.CATEGORY_SUBCAT, pValue);
393     }
394 
395     @Override
396     public MoneyWiseCategoryBaseList<?> getList() {
397         return (MoneyWiseCategoryBaseList<?>) super.getList();
398     }
399 
400     @Override
401     public MoneyWiseCategoryBase getBase() {
402         return (MoneyWiseCategoryBase) super.getBase();
403     }
404 
405     @Override
406     public int compareValues(final PrometheusDataItem pThat) {
407         /* Check the category and then the name */
408         final MoneyWiseCategoryBase myThat = (MoneyWiseCategoryBase) pThat;
409         int iDiff = MetisDataDifference.compareObject(getCategoryType(), myThat.getCategoryType());
410         if (iDiff == 0) {
411             iDiff = MetisDataDifference.compareObject(getName(), myThat.getName());
412         }
413         return iDiff;
414     }
415 
416     @Override
417     public void resolveDataSetLinks() throws OceanusException {
418         /* Update the Encryption details */
419         super.resolveDataSetLinks();
420 
421         /* Resolve parent */
422         resolveDataLink(PrometheusDataResource.DATAGROUP_PARENT, getList());
423     }
424 
425     /**
426      * Resolve links within an edit set.
427      *
428      * @throws OceanusException on error
429      */
430     protected abstract void resolveEditSetLinks() throws OceanusException;
431 
432     /**
433      * Resolve subCategory name.
434      */
435     private void resolveSubCategory() {
436         /* Set to null */
437         setValueSubCategory(null);
438 
439         /* Obtain the name */
440         final String myName = getName();
441         if (myName != null) {
442             /* Look for separator */
443             final int iIndex = myName.indexOf(STR_SEP);
444             if (iIndex != -1) {
445                 /* Access and set subCategory */
446                 final String mySub = myName.substring(iIndex + 1);
447                 setValueSubCategory(mySub);
448             }
449         }
450     }
451 
452     /**
453      * Set a new category name.
454      *
455      * @param pName the new name
456      * @throws OceanusException on error
457      */
458     public void setCategoryName(final String pName) throws OceanusException {
459         setValueName(pName);
460 
461         /* Resolve the subCategory */
462         resolveSubCategory();
463     }
464 
465     /**
466      * Set a new category name.
467      *
468      * @param pParentName the parent name
469      * @param pSubCatName the subCategory name
470      * @throws OceanusException on error
471      */
472     public void setCategoryName(final String pParentName,
473                                 final String pSubCatName) throws OceanusException {
474         setCategoryName(pParentName + STR_SEP + pSubCatName);
475     }
476 
477     /**
478      * Set a new category name.
479      *
480      * @param pName the new name
481      * @throws OceanusException on error
482      */
483     public void setSubCategoryName(final String pName) throws OceanusException {
484         /* Obtain parent */
485         final MoneyWiseCategoryBase myParent = getParentCategory();
486         final String myName = getName();
487         boolean updateChildren = false;
488 
489         /* Set name appropriately */
490         if (myParent != null) {
491             /* Access class of parent */
492             final MoneyWiseCategoryInterface myClass = myParent.getCategoryTypeClass();
493 
494             /* Handle subTotals separately */
495             if (myClass.isTotals()) {
496                 setCategoryName(pName);
497                 updateChildren = !pName.equals(myName);
498             } else {
499                 setCategoryName(myParent.getName(), pName);
500             }
501 
502             /* else this is a parent */
503         } else {
504             setCategoryName(pName);
505             if (!getCategoryTypeClass().isTotals()) {
506                 updateChildren = !pName.equals(myName);
507             }
508         }
509 
510         /* If we should update the children */
511         if (updateChildren) {
512             final MoneyWiseCategoryBaseList<?> myList = getList();
513             myList.updateChildren(myList.getBaseClass().cast(this));
514         }
515     }
516 
517     /**
518      * Set a new category type.
519      *
520      * @param pType the new type
521      */
522     public abstract void setCategoryType(PrometheusStaticDataItem pType);
523 
524     /**
525      * Set a new description.
526      *
527      * @param pDesc the description
528      * @throws OceanusException on error
529      */
530     public void setDescription(final String pDesc) throws OceanusException {
531         setValueDesc(pDesc);
532     }
533 
534     /**
535      * Set a new parent category.
536      *
537      * @param pParent the new parent
538      * @throws OceanusException on error
539      */
540     public void setParentCategory(final MoneyWiseCategoryBase pParent) throws OceanusException {
541         setValueParent(pParent);
542         final String mySubName = getSubCategory();
543         if (mySubName != null) {
544             setSubCategoryName(mySubName);
545         }
546     }
547 
548     @Override
549     public void touchUnderlyingItems() {
550         /* touch the category type referred to */
551         getCategoryType().touchItem(this);
552 
553         /* Touch parent if it exists */
554         final MoneyWiseCategoryBase myParent = getParentCategory();
555         if (myParent != null) {
556             myParent.touchItem(this);
557         }
558     }
559 
560     /**
561      * Update base category from an edited category.
562      *
563      * @param pCategory the edited category
564      */
565     public void applyBasicChanges(final MoneyWiseCategoryBase pCategory) {
566         /* Update the Name if required */
567         if (!MetisDataDifference.isEqual(getName(), pCategory.getName())) {
568             setValueName(pCategory.getNameField());
569         }
570 
571         /* Update the description if required */
572         if (!MetisDataDifference.isEqual(getDesc(), pCategory.getDesc())) {
573             setValueDesc(pCategory.getDescField());
574         }
575 
576         /* Update the parent category if required */
577         if (!MetisDataDifference.isEqual(getParentCategory(), pCategory.getParentCategory())) {
578             /* Set value */
579             setValueParent(pCategory.getParentCategory());
580         }
581     }
582 
583     @Override
584     public void adjustMapForItem() {
585         final MoneyWiseCategoryBaseList<?> myList = getList();
586         final MoneyWiseCategoryDataMap<?> myMap = myList.getDataMap();
587         myMap.adjustForItem(myList.getBaseClass().cast(this));
588     }
589 
590     @Override
591     public void touchOnUpdate() {
592         /* Reset self-touches */
593         clearTouches(getItemType());
594 
595         /* Touch parent if it exists */
596         final MoneyWiseCategoryBase myParent = getParentCategory();
597         if (myParent != null) {
598             myParent.touchItem(this);
599         }
600     }
601 
602     /**
603      * The Category Base List class.
604      *
605      * @param <T> the Category Data type
606      */
607     public abstract static class MoneyWiseCategoryBaseList<T extends MoneyWiseCategoryBase>
608             extends PrometheusEncryptedList<T> {
609         /*
610          * Report fields.
611          */
612         static {
613             MetisFieldSet.newFieldSet(MoneyWiseCategoryBaseList.class);
614         }
615 
616         /**
617          * Construct an empty CORE Category list.
618          *
619          * @param pData     the DataSet for the list
620          * @param pClass    the class of the item
621          * @param pItemType the item type
622          */
623         protected MoneyWiseCategoryBaseList(final PrometheusDataSet pData,
624                                             final Class<T> pClass,
625                                             final MoneyWiseBasicDataType pItemType) {
626             super(pClass, pData, pItemType, PrometheusListStyle.CORE);
627         }
628 
629         /**
630          * Constructor for a cloned List.
631          *
632          * @param pSource the source List
633          */
634         protected MoneyWiseCategoryBaseList(final MoneyWiseCategoryBaseList<T> pSource) {
635             super(pSource);
636         }
637 
638         @Override
639         @SuppressWarnings("unchecked")
640         public MoneyWiseCategoryDataMap<T> getDataMap() {
641             return (MoneyWiseCategoryDataMap<T>) super.getDataMap();
642         }
643 
644         @Override
645         @SuppressWarnings("unchecked")
646         public MoneyWiseDataValidatorParentDefaults<T> getValidator() {
647             return (MoneyWiseDataValidatorParentDefaults<T>) super.getValidator();
648         }
649 
650         @Override
651         public T findItemByName(final String pName) {
652             /* Access the dataMap */
653             final MoneyWiseCategoryDataMap<T> myMap = getDataMap();
654 
655             /* Use it if we have it */
656             if (myMap != null) {
657                 return myMap.findItemByName(pName);
658             }
659 
660             /* No map so we must do a slow lookUp */
661             final Iterator<T> myIterator = iterator();
662             while (myIterator.hasNext()) {
663                 final T myItem = myIterator.next();
664 
665                 /* If this is not deleted and matches */
666                 if (!myItem.isDeleted()
667                         && MetisDataDifference.isEqual(pName, myItem.getName())) {
668                     /* found it */
669                     return myItem;
670                 }
671             }
672 
673             /* Not found */
674             return null;
675         }
676 
677         /**
678          * Update Children.
679          *
680          * @param pParent the parent item
681          * @throws OceanusException on error
682          */
683         private void updateChildren(final MoneyWiseCategoryBase pParent) throws OceanusException {
684             /* Determine the id */
685             final Integer myId = pParent.getIndexedId();
686             final String myName = pParent.getName();
687 
688             /* Loop through the items */
689             final Iterator<T> myIterator = iterator();
690             while (myIterator.hasNext()) {
691                 final T myCurr = myIterator.next();
692 
693                 /* If we have a child of the parent */
694                 if (myId.equals(myCurr.getParentCategoryId())) {
695                     /* Update name and point to edit parent */
696                     myCurr.pushHistory();
697                     myCurr.setParentCategory(pParent);
698                     myCurr.setCategoryName(myName, myCurr.getSubCategory());
699                     myCurr.checkForHistory();
700                 }
701             }
702         }
703 
704         /**
705          * Resolve update set links.
706          *
707          * @throws OceanusException on error
708          */
709         public void resolveUpdateSetLinks() throws OceanusException {
710             /* Loop through the items */
711             final Iterator<T> myIterator = iterator();
712             while (myIterator.hasNext()) {
713                 final T myCurr = myIterator.next();
714                 myCurr.resolveEditSetLinks();
715             }
716         }
717 
718         @Override
719         protected MoneyWiseCategoryDataMap<T> allocateDataMap() {
720             return new MoneyWiseCategoryDataMap<>();
721         }
722     }
723 
724     /**
725      * The dataMap class.
726      *
727      * @param <T> the Category Data type
728      */
729     public static class MoneyWiseCategoryDataMap<T extends MoneyWiseCategoryBase>
730             extends PrometheusDataInstanceMap<T, String> {
731         @Override
732         @SuppressWarnings("unchecked")
733         public void adjustForItem(final PrometheusDataItem pItem) {
734             /* Access item */
735             final T myItem = (T) pItem;
736 
737             /* Adjust name count */
738             adjustForItem((T) pItem, myItem.getName());
739         }
740 
741         /**
742          * find item by name.
743          *
744          * @param pName the name to look up
745          * @return the matching item
746          */
747         public T findItemByName(final String pName) {
748             return findItemByKey(pName);
749         }
750 
751         /**
752          * Check validity of name.
753          *
754          * @param pName the name to look up
755          * @return true/false
756          */
757         public boolean validNameCount(final String pName) {
758             return validKeyCount(pName);
759         }
760 
761         /**
762          * Check availability of name.
763          *
764          * @param pName the key to look up
765          * @return true/false
766          */
767         public boolean availableName(final String pName) {
768             return availableKey(pName);
769         }
770     }
771 }