Search code examples
javastringalgorithmhashmaptreeview

Display HashMap content in tree view


I need your help because I don't find the solution in Java for my problem.

I stored in object LinkedHashMap<String, String> this content:

TAG1.TAG2.TAG11 : value1
TAG1.TAG2.TAG12 : value2
TAG1.TAG2.TAG3.TAG131 : value3
TAG1.TAG2.TAG3.TAG132 : value4
TAG1.TAG2.TAG3.TAG133 : value5
TAG1.TAG2.TAG3.TAG134 : value6
TAG1.TAG4.TAG5.TAG21 : value7
TAG1.TAG4.TAG5.TAG22 : value8
TAG1.TAG4.TAG5.TAG23 : value9
TAG6 : value10

I need to display if a tag has 2 or more children, the list of child. Here is the expected result:

TAG1.TAG2
    TAG11 : value1
    TAG12 : value2
TAG1.TAG2.TAG3
    TAG131 : value3
    TAG132 : value4
    TAG133 : value5
    TAG134 : value6
TAG1.TAG4.TAG5
    TAG21 : value7
    TAG22 : value8
    TAG23 : value9
TAG6 : value10

EDIT 14/06/2022 :

In fact, my original analyse is bad because initialy I have a XML file :

<TAG1>
 <TAG2>
  <TAG11>value1</TAG11>
  <TAG12>value2</TAG12>
  <TAG3>
   <TAG131>value3</TAG131>
   <TAG132>value4</TAG132>
   <TAG133>value5</TAG133>
   <TAG134>value6</TAG134>
  </TAG3>
 </TAG2>
 <TAG4>
  <TAG5>
   <TAG21>value7</TAG21>
   <TAG22>value8</TAG22>
   <TAG23>value9</TAG23>
  </TAG5>
 </TAG4>
</TAG1>
<TAG6>value10</TAG6>

And I created a map to store it :

TAG1.TAG2.TAG11 : value1
TAG1.TAG2.TAG12 : value2
TAG1.TAG2.TAG3.TAG131 : value3
TAG1.TAG2.TAG3.TAG132 : value4
TAG1.TAG2.TAG3.TAG133 : value5
TAG1.TAG2.TAG3.TAG134 : value6
TAG1.TAG4.TAG5.TAG21 : value7
TAG1.TAG4.TAG5.TAG22 : value8
TAG1.TAG4.TAG5.TAG23 : value9
TAG6 : value10

But, today I have a this case :

<TAG1>
 <TAG2>
  <TAG11>value1</TAG11>
  <TAG12>value2</TAG12>
  <TAG3>
   <TAG131>value3</TAG131>
   <TAG132>value4</TAG132>
   <TAG133>value5</TAG133>
   <TAG134>value6</TAG134>
  </TAG3>
  <TAG3>
   <TAG131>value11</TAG131>
   <TAG132>value12</TAG132>
   <TAG133>value13</TAG133>
   <TAG134>value14</TAG134>
  </TAG3>
 </TAG2>
 <TAG4>
  <TAG5>
   <TAG21>value7</TAG21>
   <TAG22>value8</TAG22>
   <TAG23>value9</TAG23>
  </TAG5>
 </TAG4>
</TAG1>
<TAG6>value10</TAG6>

But the Map object does not allow to store many keys (in the example many TAG3). Have you got an idea how I can resolve this problem ?


EDIT 15/06/2022 :

In fact the expected result needs to keep the original XML structure. Here the result of last sample :

TAG1.TAG2
    TAG11 : value1
    TAG12 : value2
TAG1.TAG2.TAG3
    TAG131 : value3
    TAG132 : value4
    TAG133 : value5
    TAG134 : value6
TAG1.TAG2.TAG3
    TAG131 : value11
    TAG132 : value12
    TAG133 : value13
    TAG134 : value14
TAG1.TAG4.TAG5
    TAG21 : value7
    TAG22 : value8
    TAG23 : value9
TAG6 : value10

It's to display xml more human reader.


EDIT 04/07/2022 :

I detect a problem of inconsistent with "new TreeMap<>(Comparator.comparingInt(MyTag::getAppearanceOrder)". Indeed, some MyTag object are the same AppearanceOrder, so there is a problem of inconsistent ordering. Some value in Map are so removed. To resolve I used :

map.entrySet().stream().sorted(Map.Entry.comparingByKey(. . .))

And I store the result in Map with collect().

Below the working code :

public class Main {
    public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
        //Accessing the xml file
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();

        Document document = builder.parse(new File("data.xml"));

        document.getDocumentElement().normalize();
        Element root = document.getDocumentElement();

        //Retrieving a List of records where each record contains: the original chain of tags, the numbered chain of tags and the value
        String tagSep = ".";
        List<Record> listRecords = new ArrayList<>();
        visitXMLFile(listRecords, root.getChildNodes(), tagSep, "", "", new HashMap<>());

        //Queue sorted by the numbered tag's length in descending order (from the longest to the shortest)
        PriorityQueue<Record> queue = new PriorityQueue<>(Comparator.comparing(Record::getTagNumberedLen).reversed());
        queue.addAll(listRecords);

        //Using a set to have unique numbered tags (no duplicates) to group by in the resulting map
        Set<MyTag> setMyTags = new HashSet<>();

        //Checking for each numbered tag if its largest substring is equal to any other numbered tag's beginning:
        //  - if it does, then the substring is collected as a key to group by within the final map
        //
        //  - if it doesn't, then another substring is generated from the previous substring until a matching value is found.
        //          If no value is found, then the numbered tag is collected entirely as a key for the resulting map.
        while (!queue.isEmpty()) {
            Record rec = queue.poll();

            //This loop keeps creating substrings of the current numbered tag until:
            //  - the substring matches another numbered tag's beginning
            //  - or no more substrings can be generated
            int lastIndexTagNum = rec.getTagNumbered().lastIndexOf(tagSep);
            int lastIndexTag = rec.getTag().lastIndexOf(tagSep);
            while (lastIndexTagNum > 0) {

                //Checking if the substring matches the beginning of any numbered tag except the current one
                String subStrTagNum = rec.getTagNumbered().substring(0, lastIndexTagNum);
                if (listRecords.stream().anyMatch(r -> !r.getTagNumbered().equals(rec.getTagNumbered()) && r.getTagNumbered().startsWith(subStrTagNum + tagSep))) {

                    String subStrTag = rec.getTag().substring(0, lastIndexTag);
                    int appearanceOrder = listRecords.stream().filter(r -> r.getTagNumbered().startsWith(subStrTagNum + tagSep)).map(r -> r.getAppearanceOrder()).min(Comparator.naturalOrder()).orElse(0);

                    //If a match is found then the current substring is added to the set and the substring iteration is interrupted
                    setMyTags.add(new MyTag(subStrTag, subStrTagNum + tagSep, appearanceOrder));
                    break;
                }

                //Creating a new substring from the previous substring if no match has been found
                lastIndexTagNum = rec.getTagNumbered().substring(0, lastIndexTagNum).lastIndexOf(tagSep);
                lastIndexTag = rec.getTag().substring(0, lastIndexTag).lastIndexOf(tagSep);
            }

            //If no substrings of the current numbered tag matches the beginning of any other numbered tag,
            //then the current numbered tag is collected as a key for the resulting map
            if (lastIndexTagNum < 0) {
                int appearanceOrder = listRecords.stream().filter(r -> r.getTagNumbered().startsWith(rec.getTagNumbered())).map(r -> r.getAppearanceOrder()).min(Comparator.naturalOrder()).orElse(0);
                setMyTags.add(new MyTag(rec.getTag(), rec.getTagNumbered(), appearanceOrder));
            }
        }

        //Creating a temporary resulting map (not sorted as the input)
        Map<MyTag, List<String>> mapTemp = listRecords.stream()
                .collect(Collectors.toMap(
                                rec -> {
                                    //Looking for the longest numbered tag which matches the beginning of the current record's numbered tag.
                                    //The reason why we need the longest match (i.e. the most accurate) is because some elements
                                    //may share the same parents but be on different levels, for example the values 3, 4, 5 and 6
                                    //have a key whose beginning matches both "TAG1.TAG2" and "TAG1.TAG2.TAG3", but only the longest
                                    //match is actually the right one.
                                    return setMyTags.stream().filter(mt -> rec.getTagNumbered().startsWith(mt.getTagNumbered())).max(Comparator.comparingInt(MyTag::getTagNumberedLen)).orElseThrow(() -> new RuntimeException("No key found"));
                                },
                                rec -> {
                                    //Retrieving, like above, the numbered tag that will be used to map the current value
                                    MyTag myTag = setMyTags.stream().filter(mt -> rec.getTagNumbered().startsWith(mt.getTagNumbered())).max(Comparator.comparingInt(MyTag::getTagNumberedLen)).orElseThrow(() -> new RuntimeException("No key found"));

                                    //If the new numbered tag and the record's numbered tag are equal then a List with the current value is returned
                                    if (myTag.getTagNumbered().equals(rec.getTagNumbered())) {
                                        return new ArrayList<>(List.of(rec.getValue()));
                                    } else {    //If the new numbered tag is a substring of the record's numbered tag then the rest of the current (non-numbered) tag is added to the value
                                        return new ArrayList<>(List.of(rec.getTag().substring(myTag.getTag().length() + 1) + " : " + rec.getValue()));
                                    }
                                },
                                //Handling colliding cases by merging the lists together
                                (list1, list2) -> {
                                    list1.addAll(list2);
                                    return list1;
                                }
                        )
                );

        //Creating a TreeMap whose ordering is based on the insertion order of the input
        Map<MyTag, List<String>> mapRes =
                mapTemp.entrySet().stream()
                        .sorted(Map.Entry.comparingByKey(Comparator.comparingInt(MyTag::getAppearanceOrder)))
                        .collect(Collectors.toMap(
                                Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));

        //Printing the resulting map
        for (Map.Entry<MyTag, List<String>> entry : mapRes.entrySet()) {
            System.out.println(entry.getKey());
            for (String value : entry.getValue()) {
                System.out.println("\t" + value);
            }
        }
    }

    private static void visitXMLFile(List<Record> listInput, NodeList nodeList, String tagSep, String tag, String tagNumbered, Map<String, Integer> mapTagOccurrence) {
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.hasChildNodes()) {
                String newTag = tag.isEmpty() ? node.getNodeName() : tag + tagSep + node.getNodeName();

                //Setting or incrementing the number of appearances of a tag chain
                //(sometimes a same chain of tags can be repeated, ex: TAG1.TAG2.TAG3)
                if (!mapTagOccurrence.containsKey(newTag)) {
                    mapTagOccurrence.put(newTag, 1);
                } else {
                    mapTagOccurrence.computeIfPresent(newTag, (key, val) -> val + 1);
                }

                //Creating a numbered version of the tag where its number of appearances is added at the end.
                //This is done to uniquely identify different groups of tag chain when these are repeated (ex: TAG1.TAG2.TAG3)
                String newTagNum = tagNumbered.isEmpty() ? node.getNodeName() + mapTagOccurrence.get(newTag) : tagNumbered + tagSep + node.getNodeName() + mapTagOccurrence.get(newTag);

                visitXMLFile(listInput, node.getChildNodes(), tagSep, newTag, newTagNum, mapTagOccurrence);
            } else {
                if (!node.getTextContent().trim().equals("")) {
                    int appearanceOrder = listInput.size() + 1;
                    listInput.add(new Record(tag, tagNumbered, node.getTextContent().trim(), appearanceOrder));
                }
            }
        }
    }
}

class MyTag {

    //Tag chain for the user
    private String tag;

    //Unique tag chain for identification
    private String tagNumbered;

    private int appearanceOrder;

    public MyTag(String tag, String tagNumbered, int appearanceOrder) {
        this.tag = tag;
        this.tagNumbered = tagNumbered;
        this.appearanceOrder = appearanceOrder;
    }

    public String getTag() {
        return tag;
    }

    public String getTagNumbered() {
        return tagNumbered;
    }

    public int getTagNumberedLen() {
        return tagNumbered == null ? 0 : tagNumbered.length();
    }

    public int getAppearanceOrder() {
        return appearanceOrder;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyTag tagPair = (MyTag) o;
        return Objects.equals(tagNumbered, tagPair.tagNumbered);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tagNumbered);
    }

    @Override
    public String toString() {
        return tag;
    }
}

class Record {

    //Tag chain for the user
    private String tag;

    //Unique tag chain for identification
    private String tagNumbered;

    private String value;

    private int appearanceOrder;

    public Record(String tag, String tagNumbered, String value, int appearanceOrder) {
        this.tag = tag;
        this.tagNumbered = tagNumbered;
        this.value = value;
        this.appearanceOrder = appearanceOrder;
    }

    public String getTag() {
        return tag;
    }

    public String getTagNumbered() {
        return tagNumbered;
    }

    public int getTagNumberedLen() {
        return tagNumbered == null ? 0 : tagNumbered.length();
    }

    public String getValue() {
        return value;
    }

    public int getAppearanceOrder() {
        return appearanceOrder;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Record record = (Record) o;
        return Objects.equals(tagNumbered, record.tagNumbered);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tagNumbered);
    }

    @Override
    public String toString() {
        return tag + " - " + tagNumbered + " - " + value;
    }
}

Solution

  • Edit

    At this point to answer your edited question, I had to use a List instead of a Map to store the input since multiple values share the same key and a Map<String, List<String>> wouldn't maintain the insertion order. In fact, the values from 3 to 6 would be alternated with the values from 11 to 14.

    Besides, since the same chain of tags can appear several times (ex: TAG1.TAG2.TAG3), I had to implement two custom classes: MyTag and Record.

    The first class represents a custom tag made of two fields: tag and tagNumbered. The first field holds the tag chain that must be shown to the user, while the second is used as the actual identifier to group by in the stream operation. tagNumbered is basically a copy of tag where at the end of each nested tag is added its number of appearances.

    Instead, the class Record is used to represent a value accompanied by its tag chain and numbered tag chain.

    So, the following XML is represented as follows by the respective classes:

    <x>
        <y>
            <z>value1</z>
        </y>
        <y>
            <z>value2</z>
        </y>
    </x>
    

    Record:

    Record1:
        - tag: x.y.z
        - tagNumbered: x1.y1.z1
        - value: value1
    
    Record2:
        - tag: x.y.z
        - tagNumbered: x1.y2.z1  //because y appears twice within x
        - value: value2
    

    MyTag (MyTag is created from Record):

    MyTag1:
        - tag: x.y.z
        - tagNumbered: x1.y1.z1
    
    MyTag2:
        - tag: x.y.z
        - tagNumbered: x1.y2.z1  //because y appears twice within x
    

    Here is an XML sample based on your question's input, that I've used for the code below.

    <root>
        <TAG1>
            <TAG2>
                <TAG11>value1</TAG11>
                <TAG12>value2</TAG12>
                <TAG3>
                    <TAG131>value3</TAG131>
                    <TAG132>value4</TAG132>
                    <TAG133>value5</TAG133>
                    <TAG134>value6</TAG134>
                </TAG3>
                <TAG3>
                    <TAG131>value11</TAG131>
                    <TAG132>value12</TAG132>
                    <TAG133>value13</TAG133>
                    <TAG134>value14</TAG134>
                </TAG3>
            </TAG2>
            <TAG4>
                <TAG5>
                    <TAG21>value7</TAG21>
                    <TAG22>value8</TAG22>
                    <TAG23>value9</TAG23>
                </TAG5>
            </TAG4>
        </TAG1>
        <TAG6>value10</TAG6>
    </root>
    

    Original Answer Updated

    The first part of the problem consists in creating a List<Record> while reading from the XML file which is achieved with the visitXMLFile method.

    After reading the records from the file, we need to create a Set of unique numbered tag chains to identify each group of values. This is actually done with a Set<MyTag>; however MyTag's equals() and hashCode() are based exclusively on tagNumbered.

    After creating the Set of unique numbered tags, we need to stream the input list of entries with a single operation: collect(Collectors.toMap()). In this operation, each record is mapped to a MyTag (i.e., a numbered tag) of the Set previously created.

    Finally, to maintain the original insertion order, the resulting Map has been implemented as a TreeMap initialized with a Comparator defined on the order of the input list's records.

    Here is an implementation with detailed comments explaining the whole logic step by step:

    public class Main {
        public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
            //Accessing the xml file
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
    
            Document document = builder.parse(new File("data.xml"));
    
            document.getDocumentElement().normalize();
            Element root = document.getDocumentElement();
    
            //Retrieving a List of records where each record contains: the original chain of tags, the numbered chain of tags and the value
            String tagSep = ".";
            List<Record> listRecords = new ArrayList<>();
            visitXMLFile(listRecords, root.getChildNodes(), tagSep, "", "", new HashMap<>());
    
            //Queue sorted by the numbered tag's length in descending order (from the longest to the shortest)
            PriorityQueue<Record> queue = new PriorityQueue<>(Comparator.comparing(Record::getTagNumberedLen).reversed());
            queue.addAll(listRecords);
    
            //Using a set to have unique numbered tags (no duplicates) to group by in the resulting map
            Set<MyTag> setMyTags = new HashSet<>();
    
            //Checking for each numbered tag if its largest substring is equal to any other numbered tag's beginning:
            //  - if it does, then the substring is collected as a key to group by within the final map
            //
            //  - if it doesn't, then another substring is generated from the previous substring until a matching value is found.
            //          If no value is found, then the numbered tag is collected entirely as a key for the resulting map.
            while (!queue.isEmpty()) {
                Record rec = queue.poll();
    
                //This loop keeps creating substrings of the current numbered tag until:
                //  - the substring matches another numbered tag's beginning
                //  - or no more substrings can be generated
                int lastIndexTagNum = rec.getTagNumbered().lastIndexOf(tagSep);
                int lastIndexTag = rec.getTag().lastIndexOf(tagSep);
                while (lastIndexTagNum > 0) {
    
                    //Checking if the substring matches the beginning of any numbered tag except the current one
                    String subStrTagNum = rec.getTagNumbered().substring(0, lastIndexTagNum);
                    if (listRecords.stream().anyMatch(r -> !r.getTagNumbered().equals(rec.getTagNumbered()) && r.getTagNumbered().startsWith(subStrTagNum + tagSep))) {
    
                        String subStrTag = rec.getTag().substring(0, lastIndexTag);
                        int appearanceOrder = listRecords.stream().filter(r -> r.getTagNumbered().startsWith(subStrTagNum + tagSep)).map(r -> r.getAppearanceOrder()).min(Comparator.naturalOrder()).orElse(0);
    
                        //If a match is found then the current substring is added to the set and the substring iteration is interrupted
                        setMyTags.add(new MyTag(subStrTag, subStrTagNum + tagSep, appearanceOrder));
                        break;
                    }
    
                    //Creating a new substring from the previous substring if no match has been found
                    lastIndexTagNum = rec.getTagNumbered().substring(0, lastIndexTagNum).lastIndexOf(tagSep);
                    lastIndexTag = rec.getTag().substring(0, lastIndexTag).lastIndexOf(tagSep);
                }
    
                //If no substrings of the current numbered tag matches the beginning of any other numbered tag,
                //then the current numbered tag is collected as a key for the resulting map
                if (lastIndexTagNum < 0) {
                    int appearanceOrder = listRecords.stream().filter(r -> r.getTagNumbered().startsWith(rec.getTagNumbered())).map(r -> r.getAppearanceOrder()).min(Comparator.naturalOrder()).orElse(0);
                    setMyTags.add(new MyTag(rec.getTag(), rec.getTagNumbered(), appearanceOrder));
                }
            }
    
            //Creating a temporary resulting map (not sorted as the input)
            Map<MyTag, List<String>> mapTemp = listRecords.stream()
                    .collect(Collectors.toMap(
                                    rec -> {
                                        //Looking for the longest numbered tag which matches the beginning of the current record's numbered tag.
                                        //The reason why we need the longest match (i.e. the most accurate) is because some elements
                                        //may share the same parents but be on different levels, for example the values 3, 4, 5 and 6
                                        //have a key whose beginning matches both "TAG1.TAG2" and "TAG1.TAG2.TAG3", but only the longest
                                        //match is actually the right one.
                                        return setMyTags.stream().filter(mt -> rec.getTagNumbered().startsWith(mt.getTagNumbered())).max(Comparator.comparingInt(MyTag::getTagNumberedLen)).orElseThrow(() -> new RuntimeException("No key found"));
                                    },
                                    rec -> {
                                        //Retrieving, like above, the numbered tag that will be used to map the current value
                                        MyTag myTag = setMyTags.stream().filter(mt -> rec.getTagNumbered().startsWith(mt.getTagNumbered())).max(Comparator.comparingInt(MyTag::getTagNumberedLen)).orElseThrow(() -> new RuntimeException("No key found"));
    
                                        //If the new numbered tag and the record's numbered tag are equal then a List with the current value is returned
                                        if (myTag.getTagNumbered().equals(rec.getTagNumbered())) {
                                            return new ArrayList<>(List.of(rec.getValue()));
                                        } else {    //If the new numbered tag is a substring of the record's numbered tag then the rest of the current (non-numbered) tag is added to the value
                                            return new ArrayList<>(List.of(rec.getTag().substring(myTag.getTag().length() + 1) + " : " + rec.getValue()));
                                        }
                                    },
                                    //Handling colliding cases by merging the lists together
                                    (list1, list2) -> {
                                        list1.addAll(list2);
                                        return list1;
                                    }
                            )
                    );
    
            //Creating a TreeMap whose ordering is based on the insertion order of the input
            Map<MyTag, List<String>> mapRes = new TreeMap<>(Comparator.comparingInt(MyTag::getAppearanceOrder));
            mapRes.putAll(mapTemp);
    
            //Printing the resulting map
            for (Map.Entry<MyTag, List<String>> entry : mapRes.entrySet()) {
                System.out.println(entry.getKey());
                for (String value : entry.getValue()) {
                    System.out.println("\t" + value);
                }
            }
        }
    
        private static void visitXMLFile(List<Record> listInput, NodeList nodeList, String tagSep, String tag, String tagNumbered, Map<String, Integer> mapTagOccurrence) {
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node.hasChildNodes()) {
                    String newTag = tag.isEmpty() ? node.getNodeName() : tag + tagSep + node.getNodeName();
    
                    //Setting or incrementing the number of appearances of a tag chain
                    //(sometimes a same chain of tags can be repeated, ex: TAG1.TAG2.TAG3)
                    if (!mapTagOccurrence.containsKey(newTag)) {
                        mapTagOccurrence.put(newTag, 1);
                    } else {
                        mapTagOccurrence.computeIfPresent(newTag, (key, val) -> val + 1);
                    }
    
                    //Creating a numbered version of the tag where its number of appearances is added at the end.
                    //This is done to uniquely identify different groups of tag chain when these are repeated (ex: TAG1.TAG2.TAG3)
                    String newTagNum = tagNumbered.isEmpty() ? node.getNodeName() + mapTagOccurrence.get(newTag) : tagNumbered + tagSep + node.getNodeName() + mapTagOccurrence.get(newTag);
    
                    visitXMLFile(listInput, node.getChildNodes(), tagSep, newTag, newTagNum, mapTagOccurrence);
                } else {
                    if (!node.getTextContent().trim().equals("")) {
                        int appearanceOrder = listInput.size() + 1;
                        listInput.add(new Record(tag, tagNumbered, node.getTextContent().trim(), appearanceOrder));
                    }
                }
            }
        }
    }
    
    class MyTag {
    
        //Tag chain for the user
        private String tag;
    
        //Unique tag chain for identification
        private String tagNumbered;
    
        private int appearanceOrder;
    
        public MyTag(String tag, String tagNumbered, int appearanceOrder) {
            this.tag = tag;
            this.tagNumbered = tagNumbered;
            this.appearanceOrder = appearanceOrder;
        }
    
        public String getTag() {
            return tag;
        }
    
        public String getTagNumbered() {
            return tagNumbered;
        }
    
        public int getTagNumberedLen() {
            return tagNumbered == null ? 0 : tagNumbered.length();
        }
    
        public int getAppearanceOrder() {
            return appearanceOrder;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            MyTag tagPair = (MyTag) o;
            return Objects.equals(tagNumbered, tagPair.tagNumbered);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(tagNumbered);
        }
    
        @Override
        public String toString() {
            return tag;
        }
    }
    
    class Record {
    
        //Tag chain for the user
        private String tag;
    
        //Unique tag chain for identification
        private String tagNumbered;
    
        private String value;
    
        private int appearanceOrder;
    
        public Record(String tag, String tagNumbered, String value, int appearanceOrder) {
            this.tag = tag;
            this.tagNumbered = tagNumbered;
            this.value = value;
            this.appearanceOrder = appearanceOrder;
        }
    
        public String getTag() {
            return tag;
        }
    
        public String getTagNumbered() {
            return tagNumbered;
        }
    
        public int getTagNumberedLen() {
            return tagNumbered == null ? 0 : tagNumbered.length();
        }
    
        public String getValue() {
            return value;
        }
    
        public int getAppearanceOrder() {
            return appearanceOrder;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Record record = (Record) o;
            return Objects.equals(tagNumbered, record.tagNumbered);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(tagNumbered);
        }
    
        @Override
        public String toString() {
            return tag + " - " + tagNumbered + " - " + value;
        }
    }
    

    Output

    TAG1.TAG2
        TAG11 : value1
        TAG12 : value2
    TAG1.TAG2.TAG3
        TAG131 : value3
        TAG132 : value4
        TAG133 : value5
        TAG134 : value6
    TAG1.TAG2.TAG3
        TAG131 : value11
        TAG132 : value12
        TAG133 : value13
        TAG134 : value14
    TAG1.TAG4.TAG5
        TAG21 : value7
        TAG22 : value8
        TAG23 : value9
    TAG6
        value10