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