ArduinoParser.java

/*******************************************************************************
 * jArduino: Arduino C++ Code Generation From Java
 * Copyright 2020 Tony Washer
 *
 * 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 net.sourceforge.jarduino.message;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

import net.sourceforge.jarduino.ArduinoException;

/**
 * Parser.
 */
public final class ArduinoParser {
    /**
     * Number format (US format to match .dbc standard).
     */
    private static final NumberFormat FORMAT = NumberFormat.getInstance(Locale.US);

    /**
     * Private constructor.
     */
    private ArduinoParser() {
    }

    /**
     * parse a file.
     * @param pDBCFile the DBCFile
     * @param pCharSet the character set to use
     * @return the parsed system
     * @throws ArduinoException on error
     */
    public static ArduinoSystem parseFile(final File pDBCFile,
                                          final Charset pCharSet) throws ArduinoException {
        try (FileInputStream myStream = new FileInputStream(pDBCFile)) {
            return parseStream(pDBCFile.getName().replace(".dbc", ""), pCharSet, myStream);
        } catch (IOException e) {
            throw new ArduinoException("Failed to open file", e);
        }
    }

    /**
     * parse an input stream.
     * @param pName the name
     * @param pCharSet the character set to use
     * @param pInput the input stream
     * @return the parsed system
     * @throws ArduinoException on error
     */
    public static ArduinoSystem parseStream(final String pName,
                                            final Charset pCharSet,
                                            final InputStream pInput) throws ArduinoException {
        /* Protect against exceptions */
        try (InputStreamReader myInputReader = new InputStreamReader(pInput, pCharSet);
             BufferedReader myReader = new BufferedReader(myInputReader)) {

            /* The current message */
            ArduinoSystem mySystem = null;
            ArduinoMessage myMessage = null;

            /* Read the header entry */
            boolean systemFound = false;
            for (;;) {
                /* Read next line */
                String myLine = readNextLine(myReader);
                if (myLine == null) {
                    break;
                }
                myLine = myLine.trim();

                /* If we have not yet found the system */
                if (!systemFound) {
                    /* if we have found the system */
                    if (myLine.startsWith(ArduinoNode.MARKER + ArduinoChar.COLON)) {
                        /* Allocate the system and note the fact */
                        mySystem = ArduinoSystem.parseSystem(pName, myLine);
                        systemFound = true;
                    }

                    /* else if we have a message */
                } else {
                    /* Parse the line */
                    myMessage = parseLine(mySystem, myMessage, myLine);
                }
            }

            /* return the system */
            return mySystem;

            /* Handle exceptions */
        } catch (IOException e) {
            throw new ArduinoException("Failed to read file", e);
        }
    }

    /**
     * read next line allowing for LF embedded in quotes.
     * @param pReader the input reader
     * @return the next line
     * @throws ArduinoException on error
     */
    private static String readNextLine(final BufferedReader pReader) throws ArduinoException {
        /* Protect against exceptions */
        try {
            String myLine = null;

            /* Loop reading lines */
            for (;;) {
                /* Read next line */
                final String myPart = pReader.readLine();
                if (myPart == null) {
                    return myLine;
                }

                /* Append to any existing line */
                myLine = myLine == null
                         ? myPart
                         : myLine + ArduinoChar.LF + myPart;

                /* Count the number of quotes in the line */
                final int myCount = countQuotes(myLine);

                /* Line is complete if all quotes are completed */
                if (myCount % 2 == 0) {
                    return myLine;
                }
            }

            /* Handle exceptions */
        } catch (IOException e) {
            throw new ArduinoException("Failed to read line", e);
        }
    }

    /**
     * Count the number of quotes in the line.
     * @param pLine the current line
     * @return the count of quotes
     */
    private static int countQuotes(final String pLine) {
       /* Handle no quote at all  */
        int myIndex = pLine.indexOf(ArduinoChar.QUOTE);
        if (myIndex == -1) {
            return 0;
        }

        /* Loop looking for more quotes */
        for (int myCount = 1; ; myCount++) {
            myIndex = pLine.indexOf(ArduinoChar.QUOTE, myIndex + 1);
            if (myIndex == -1) {
                return myCount;
            }
        }
    }

    /**
     * process line.
     * @param pSystem the system
     * @param pMessage the current message
     * @param pLine the line
     * @return the current message
     * @throws ArduinoException on error
     */
    public static ArduinoMessage parseLine(final ArduinoSystem pSystem,
                                           final ArduinoMessage pMessage,
                                           final String pLine) throws ArduinoException {
        /* Access current message and token */
        ArduinoMessage myMessage = pMessage;
        switch (nextToken(pLine)) {
            /* Message */
            case ArduinoMessage.MARKER:
                myMessage = ArduinoMessage.parseMessage(pSystem, pLine);
                break;

            /* Signal */
            case ArduinoSignal.MARKER:
                if (myMessage != null) {
                    ArduinoSignal.parseSignal(myMessage, pLine);
                }
                break;

            /* Comment */
            case ArduinoComments.MARKER:
                ArduinoComments.parseComment(pSystem, pLine);
                break;

            /* attributeDef */
            case ArduinoAttributes.MARKER_DEF:
                ArduinoAttributes.parseAttributeDef(pSystem, ArduinoAttributes.MARKER_DEF, pLine);
                break;

            /* attributeDefault */
            case ArduinoAttributes.MARKER_DEFAULT:
                ArduinoAttributes.parseAttributeDefault(pSystem, ArduinoAttributes.MARKER_DEFAULT, pLine);
                break;

            /* attribute */
            case ArduinoAttributes.MARKER:
                ArduinoAttributes.parseAttribute(pSystem, ArduinoAttributes.MARKER, pLine);
                break;

            /* attributeRelDef */
            case ArduinoAttributes.MARKER_REL_DEF:
                ArduinoAttributes.parseAttributeDef(pSystem, ArduinoAttributes.MARKER_REL_DEF, pLine);
                break;

            /* attributeRelDefault */
            case ArduinoAttributes.MARKER_REL_DEFAULT:
                ArduinoAttributes.parseAttributeDefault(pSystem, ArduinoAttributes.MARKER_REL_DEFAULT, pLine);
                break;

            /* attributeRel */
            case ArduinoAttributes.MARKER_REL:
                ArduinoAttributes.parseAttribute(pSystem, ArduinoAttributes.MARKER_REL, pLine);
                break;

            /* xmitters */
            case ArduinoNode.MARKER_TX:
                ArduinoNode.parseXmitNodes(pSystem, pLine);
                break;

            /* values */
            case ArduinoValues.MARKER:
                ArduinoValues.parseValues(pSystem, pLine);
                break;

                /* Ignore if we do not recognise */
            default:
                break;
        }

        /* return the current message */
        return myMessage;
    }

    /**
     * Obtain the next token.
     * @param pSource the current line
     * @return the next token
     */
    static String nextToken(final String pSource) {
        final int myLen = pSource.length();
        for (int i = 0; i < myLen; i++) {
            if (Character.isWhitespace(pSource.charAt(i))) {
                return pSource.substring(0, i);
            }
        }
        return pSource;
    }

    /**
     * Obtain the next token which must be between quotes.
     * @param pSource the current line
     * @return the next quoted token
     * @throws ArduinoParserException on error
     */
    static String nextQuotedToken(final String pSource) throws ArduinoParserException {
        /* Token must begin with quote */
        if (pSource.length() < 2 || pSource.charAt(0) != ArduinoChar.QUOTE) {
            throw new ArduinoParserException("Missing start quote", pSource);
        }

        /* Must have another quote */
        final int myIndex = pSource.indexOf(ArduinoChar.QUOTE, 1);
        if (myIndex == -1) {
            throw new ArduinoParserException("Missing end quote", pSource);
        }

        /* Return the token */
        return pSource.substring(1, myIndex);
    }

    /**
     * Strip the token from the line.
     * @param pSource the current line
     * @param pToken the starting token
     * @return the stripped line
     */
    static String stripToken(final String pSource,
                             final String pToken) {
        return pSource.startsWith(pToken)
               ? pSource.substring(pToken.length()).trim()
               : pSource;
    }

    /**
     * Strip the quoted token from the line.
     * @param pSource the current line
     * @param pToken the starting token
     * @return the stripped line
     */
    static String stripQuotedToken(final String pSource,
                                   final String pToken) {
        return stripToken(pSource.substring(1), pToken).substring(1).trim();
    }

    /**
     * Parse number.
     * @param pNumberDef the number representation
     * @return the number
     * @throws ArduinoParserException on error
     */
    static Number parseNumber(final String pNumberDef) throws ArduinoParserException {
        /* Protect against exceptions */
        try {
            /* Obtain the numberDef as UpperCase and without + */
            final String myNumber = pNumberDef.toUpperCase().replace('+', '0');

            /* Parse the number */
            return FORMAT.parse(myNumber);

            /* Catch parsing errors */
        } catch (ParseException e) {
            throw new ArduinoParserException("Failed to parse number", pNumberDef);
        }
    }

    /**
     * Obtain the number of decimals in a number.
     * @param pNumberDef the number representation (US format)
     * @return the number of decimals
     * @throws ArduinoParserException on error
     */
    static int determineNumDecimals(final String pNumberDef) throws ArduinoParserException {
        /* Protect against exceptions */
        try {
            /* Access the definition */
            String myDef = pNumberDef;
            int numDecimals = 0;

            /* Look for Exponential representation */
            int myIndex = pNumberDef.indexOf('e');
            if (myIndex == -1) {
                myIndex = pNumberDef.indexOf('E');
            }

            /* If we found exponential representation */
            if (myIndex != -1) {
                numDecimals = -Integer.parseInt(myDef.substring(myIndex + 1));
                myDef = myDef.substring(0, myIndex);
            }

            /* Look for Decimal representation */
            myIndex = myDef.indexOf(ArduinoChar.DEC);
            if (myIndex != -1) {
                numDecimals += myDef.substring(myIndex + 1).length();
            }

            /* Return the number of decimals */
            return numDecimals;

            /* Catch parsing errors */
        } catch (NumberFormatException e) {
            throw new ArduinoParserException("Failed to parse number", pNumberDef);
        }
    }

    /**
     * Parser exception.
     */
    static final class ArduinoParserException extends Exception {
        /**
         * Serial versionID.
         */
        private static final long serialVersionUID = 8237660286012917142L;

        /**
         * The detail.
         */
        private final String theDetail;

        /**
         * Constructor.
         * @param pMessage the message
         * @param pDetail the detail
         */
        ArduinoParserException(final String pMessage,
                               final String pDetail) {
            super(pMessage);
            theDetail = pDetail;
        }

        /**
         * Obtain the detail.
         * @return the detail
         */
        public String getDetail() {
            return theDetail;
        }
    }
}