<commit_before>/* -*- mode: java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- */
/*
 * Copyright 2013 Real Logic Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package uk.co.real_logic.sbe.xml;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static uk.co.real_logic.sbe.xml.XmlSchemaParser.*;

/**
 * An SBE message containing a list of {@link Message.Field} objects and SBE message attributes.
 * <p/>
 * What is difference between {@link Message} and the Intermediate Representation (IR)?
 * <ul>
 * <li>IR is intentionally platform, schema, and language independent.</li>
 * <li>IR is abstract layout and metadata only.</li>
 * <li>IR is a flat representation without cycles or hierarchy.</li>
 * <li>Message is FIX/SBE XML Schema specific.</li>
 * </ul>
 */
public class Message
{
    private static final String FIELD_OR_GROUP_OR_DATA_EXPR = "field|group|data";

    private final long id;
    private final String name;
    private final String description;
    private final long blockLength;
    private final List<Field> fieldList;
    private final String fixMsgType;
    private int irIdCursor = 1;

    /**
     * Construct a new message from XML Schema.
     *
     * @param messageNode   from the XML Schema Parsing
     * @param typeByNameMap holding type information for message
     */
    public Message(final Node messageNode, final Map<String, Type> typeByNameMap)
        throws XPathExpressionException, IllegalArgumentException
    {
        /*
         * message
         * - name (required) - unique?
         *
         * field
         * - name (required) - unique within message?
         * - id (required) - unique within message?
         *
         * group
         * - name (required) - unique within message? field for num entries must precede it!
         */
        id = Long.parseLong(getAttributeValue(messageNode, "id"));                        // required
        name = getAttributeValue(messageNode, "name");                                    // required
        description = getAttributeValueOrNull(messageNode, "description");                // optional
        blockLength = Long.parseLong(getAttributeValue(messageNode, "blockLength", "0")); // 0 means not set
        fixMsgType = getAttributeValueOrNull(messageNode, "fixMsgType");                  // optional

        fieldList = parseFieldsAndGroups(messageNode, typeByNameMap);
    }

    private List<Field> parseFieldsAndGroups(final Node node, final Map<String, Type> typeByNameMap)
        throws XPathExpressionException, IllegalArgumentException
    {
        XPath xPath = XPathFactory.newInstance().newXPath();
        NodeList list = (NodeList)xPath.compile(FIELD_OR_GROUP_OR_DATA_EXPR).evaluate(node, XPathConstants.NODESET);

        List<Field> fieldList = new ArrayList<>();
        Map<String, Field> entryCountFieldMap = new HashMap<>();  // used for holding entry count fields and matching up
        Map<Integer, Field> lengthFieldMap = new HashMap<>();    // used for holding length fields and matching up

        for (int i = 0, size = list.getLength(); i < size; i++)
        {
            Field field;

            final String nodeName = list.item(i).getNodeName();
            switch (nodeName)
            {
                case "group":
                    /*
                     * must search for previously parsed field that has groupName = to name
                     * (can this map be only visible on the stack?)
                     * must exist as it had to be placed before the group.
                     */
                    field = new Field(list.item(i), getAttributeValue(list.item(i), "name"));
                    Field entryCountField = entryCountFieldMap.get(field.getName());

                    if (entryCountField == null)
                    {
                        throw new IllegalArgumentException("could not find entry count field for group: " + field.getName());
                    }

                    field.setEntryCountField(entryCountField);
                    entryCountField.setGroupField(field);

                    field.setIrId(irIdCursor++);
                    field.setXRefIrId(entryCountField.getIrId());
                    entryCountField.setXRefIrId(field.getIrId());

                    entryCountFieldMap.remove(field.getName()); // remove field so that it can't be reused as this level

                    field.setGroupFieldList(parseFieldsAndGroups(list.item(i), typeByNameMap)); // recursive call
                    break;

                case "field":
                    field = new Field(list.item(i),
                                      getAttributeValue(list.item(i), "name"),
                                      Integer.parseInt(getAttributeValue(list.item(i), "id")),
                                      lookupType(typeByNameMap, getAttributeValue(list.item(i), "type")));

                    if (field.getGroupName() != null)
                    {
                        entryCountFieldMap.put(field.getGroupName(), field);
                        field.setIrId(irIdCursor++);
                    }

                    if (field.getRefId() != Field.INVALID_ID)
                    {
                        lengthFieldMap.put(Integer.valueOf(field.getRefId()), field);
                        field.setIrId(irIdCursor++);
                    }
                    break;

                case "data":
                    field = new Field(list.item(i),
                                      getAttributeValue(list.item(i), "name"),
                                      Integer.parseInt(getAttributeValue(list.item(i), "id")),
                                      lookupType(typeByNameMap, getAttributeValue(list.item(i), "type")));

                    Field lengthField = lengthFieldMap.get(Integer.valueOf(field.getId()));
                    if (lengthField == null)
                    {
                        throw new IllegalArgumentException("could not find length field for data field: " + field.getName());
                    }

                    field.setLengthField(lengthField);
                    lengthField.setDataField(field);

                    field.setIrId(irIdCursor++);
                    field.setXRefIrId(lengthField.getIrId());
                    lengthField.setXRefIrId(field.getIrId());

                    lengthFieldMap.remove(Integer.valueOf(field.getId())); // remove field so that it can be reused
                    break;

                default:
                    throw new IllegalStateException("Unknown node name: " + nodeName);
            }

            fieldList.add(field);
        }
        /*
         * TODO: if the entryCountMap is not empty, then it means something didn't get matched up... warning?
         * TODO: same for lengthFieldMap
         */
        return fieldList;
    }

    /** static method to encapsulate exception for them type does not exist. */
    private static Type lookupType(final Map<String, Type> typeByNameMap, final String name)
        throws IllegalArgumentException
    {
        Type type = typeByNameMap.get(name);
        if (type == null)
        {
            throw new IllegalArgumentException("Type does not exist for name: " + name);
        }

        return type;
    }

    /**
     * Return the template id of the message
     *
     * @return id of the message
     */<commit_msg>added checks for specifying field elements after data/group and group after data<commit_after>/* -*- mode: java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- */
/*
 * Copyright 2013 Real Logic Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package uk.co.real_logic.sbe.xml;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static uk.co.real_logic.sbe.xml.XmlSchemaParser.*;

/**
 * An SBE message containing a list of {@link Message.Field} objects and SBE message attributes.
 * <p/>
 * What is difference between {@link Message} and the Intermediate Representation (IR)?
 * <ul>
 * <li>IR is intentionally platform, schema, and language independent.</li>
 * <li>IR is abstract layout and metadata only.</li>
 * <li>IR is a flat representation without cycles or hierarchy.</li>
 * <li>Message is FIX/SBE XML Schema specific.</li>
 * </ul>
 */
public class Message
{
    private static final String FIELD_OR_GROUP_OR_DATA_EXPR = "field|group|data";

    private final long id;
    private final String name;
    private final String description;
    private final long blockLength;
    private final List<Field> fieldList;
    private final String fixMsgType;
    private int irIdCursor = 1;

    /**
     * Construct a new message from XML Schema.
     *
     * @param messageNode   from the XML Schema Parsing
     * @param typeByNameMap holding type information for message
     */
    public Message(final Node messageNode, final Map<String, Type> typeByNameMap)
        throws XPathExpressionException, IllegalArgumentException
    {
        /*
         * message
         * - name (required) - unique?
         *
         * field
         * - name (required) - unique within message?
         * - id (required) - unique within message?
         *
         * group
         * - name (required) - unique within message? field for num entries must precede it!
         */
        id = Long.parseLong(getAttributeValue(messageNode, "id"));                        // required
        name = getAttributeValue(messageNode, "name");                                    // required
        description = getAttributeValueOrNull(messageNode, "description");                // optional
        blockLength = Long.parseLong(getAttributeValue(messageNode, "blockLength", "0")); // 0 means not set
        fixMsgType = getAttributeValueOrNull(messageNode, "fixMsgType");                  // optional

        fieldList = parseFieldsAndGroups(messageNode, typeByNameMap);
    }

    private List<Field> parseFieldsAndGroups(final Node node, final Map<String, Type> typeByNameMap)
        throws XPathExpressionException, IllegalArgumentException
    {
        XPath xPath = XPathFactory.newInstance().newXPath();
        NodeList list = (NodeList)xPath.compile(FIELD_OR_GROUP_OR_DATA_EXPR).evaluate(node, XPathConstants.NODESET);
        int numGroupEncountered = 0, numDataEncountered = 0;

        List<Field> fieldList = new ArrayList<>();
        Map<String, Field> entryCountFieldMap = new HashMap<>();  // used for holding entry count fields and matching up
        Map<Integer, Field> lengthFieldMap = new HashMap<>();    // used for holding length fields and matching up

        for (int i = 0, size = list.getLength(); i < size; i++)
        {
            Field field;

            final String nodeName = list.item(i).getNodeName();
            switch (nodeName)
            {
                case "group":
                    field = new Field(list.item(i), getAttributeValue(list.item(i), "name"));
                    Field entryCountField = entryCountFieldMap.get(field.getName());

                    if (entryCountField == null)
                    {
                        throw new IllegalArgumentException("could not find entry count field for group: " + field.getName());
                    }
                    else if (numDataEncountered > 0)
                    {
                        throw new IllegalArgumentException("group specified after data specified");
                    }

                    field.setEntryCountField(entryCountField);
                    entryCountField.setGroupField(field);

                    field.setIrId(irIdCursor++);
                    field.setXRefIrId(entryCountField.getIrId());
                    entryCountField.setXRefIrId(field.getIrId());

                    entryCountFieldMap.remove(field.getName()); // remove field so that it can't be reused as this level

                    field.setGroupFieldList(parseFieldsAndGroups(list.item(i), typeByNameMap)); // recursive call

                    numGroupEncountered++;
                    break;

                case "field":
                    field = new Field(list.item(i),
                                      getAttributeValue(list.item(i), "name"),
                                      Integer.parseInt(getAttributeValue(list.item(i), "id")),
                                      lookupType(typeByNameMap, getAttributeValue(list.item(i), "type")));

                    if (field.getGroupName() != null)
                    {
                        entryCountFieldMap.put(field.getGroupName(), field);
                        field.setIrId(irIdCursor++);
                    }

                    if (field.getRefId() != Field.INVALID_ID)
                    {
                        lengthFieldMap.put(Integer.valueOf(field.getRefId()), field);
                        field.setIrId(irIdCursor++);
                    }

                    if (numGroupEncountered > 0 || numDataEncountered > 0)
                    {
                        throw new IllegalArgumentException("field specified after group or data specified");
                    }
                    break;

                case "data":
                    field = new Field(list.item(i),
                                      getAttributeValue(list.item(i), "name"),
                                      Integer.parseInt(getAttributeValue(list.item(i), "id")),
                                      lookupType(typeByNameMap, getAttributeValue(list.item(i), "type")));

                    Field lengthField = lengthFieldMap.get(Integer.valueOf(field.getId()));
                    if (lengthField == null)
                    {
                        throw new IllegalArgumentException("could not find length field for data field: " + field.getName());
                    }

                    field.setLengthField(lengthField);
                    lengthField.setDataField(field);

                    field.setIrId(irIdCursor++);
                    field.setXRefIrId(lengthField.getIrId());
                    lengthField.setXRefIrId(field.getIrId());

                    lengthFieldMap.remove(Integer.valueOf(field.getId())); // remove field so that it can be reused
                    numDataEncountered++;
                    break;

                default:
                    throw new IllegalStateException("Unknown node name: " + nodeName);
            }

            fieldList.add(field);
        }
        // TODO: if the entryCountMap is not empty, then it means something didn't get matched up... warning? same for lengthFieldMap
        return fieldList;
    }

    /** static method to encapsulate exception for them type does not exist. */
    private static Type lookupType(final Map<String, Type> typeByNameMap, final String name)
        throws IllegalArgumentException
    {
        Type type = typeByNameMap.get(name);
        if (type == null)
        {
            throw new IllegalArgumentException("Type does not exist for name: " + name);
        }

        return type;
    }

    /**
     * Return the template id of the message
     *
     * @return id of the message
     */
{'fraction_non_alphanumeric': 0.093862167982771, 'fraction_numerical': 0.002213448193347691, 'mean_word_length': 1.9225524475524476, 'pattern_counts': {'":': 6, '<': 49, '<?xml version=': 0, '>': 50, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4256193', 'n_tokens_mistral': 4205, 'n_tokens_neox': 4031, 'n_words': 1199}