001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.euclidean.threed.obj;
018
019import java.util.ArrayList;
020import java.util.List;
021
022import org.apache.commons.geometry.euclidean.threed.Vector3D;
023import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
024
025/** Abstract base class for OBJ parsing functionality.
026 */
027public abstract class AbstractObjParser {
028
029    /** Text parser instance. */
030    private final SimpleTextParser parser;
031
032    /** The current (most recently parsed) keyword. */
033    private String currentKeyword;
034
035    /** Construct a new instance for parsing OBJ content from the given text parser.
036     * @param parser text parser to read content from
037     */
038    protected AbstractObjParser(final SimpleTextParser parser) {
039        this.parser = parser;
040    }
041
042    /** Get the current keyword, meaning the keyword most recently parsed via the {@link #nextKeyword()}
043     * method. Null is returned if parsing has not started or the end of the content has been reached.
044     * @return the current keyword or null if parsing has not started or the end
045     *      of the content has been reached
046     */
047    public String getCurrentKeyword() {
048        return currentKeyword;
049    }
050
051    /** Advance the parser to the next keyword, returning true if a keyword has been found
052     * and false if the end of the content has been reached. Keywords consist of alphanumeric
053     * strings placed at the beginning of lines. Comments and blank lines are ignored.
054     * @return true if a keyword has been found and false if the end of content has been reached
055     * @throws IllegalStateException if invalid content is found
056     * @throws java.io.UncheckedIOException if an I/O error occurs
057     */
058    public boolean nextKeyword() {
059        currentKeyword = null;
060
061        // advance to the next line if not at the start of a line
062        if (parser.getColumnNumber() != 1) {
063            discardDataLine();
064        }
065
066        // search for the next keyword
067        while (currentKeyword == null && parser.hasMoreCharacters()) {
068            if (!nextDataLineContent() ||
069                    parser.peekChar() == ObjConstants.COMMENT_CHAR) {
070                // use a standard line discard here so we don't interpret line continuations
071                // within comments; the interpreted OBJ content should be the same regardless
072                // of the presence of comments
073                parser.discardLine();
074            } else if (parser.getColumnNumber() != 1) {
075                throw parser.parseError("non-blank lines must begin with an OBJ keyword or comment character");
076            } else if (!readKeyword()) {
077                throw parser.unexpectedToken("OBJ keyword");
078            } else {
079                final String keywordValue = parser.getCurrentToken();
080
081                handleKeyword(keywordValue);
082
083                currentKeyword = keywordValue;
084
085                // advance past whitespace to the next data value
086                discardDataLineWhitespace();
087            }
088        }
089
090        return currentKeyword != null;
091    }
092
093    /** Read the remaining content on the current data line, taking line continuation characters into
094     * account.
095     * @return remaining content on the current data line or null if the end of the content has
096     *      been reached
097     * @throws java.io.UncheckedIOException if an I/O error occurs
098     */
099    public String readDataLine() {
100        parser.nextWithLineContinuation(
101                ObjConstants.LINE_CONTINUATION_CHAR,
102                SimpleTextParser::isNotNewLinePart)
103            .discardNewLineSequence();
104
105        return parser.getCurrentToken();
106    }
107
108    /** Discard remaining content on the current data line, taking line continuation characters into
109     * account.
110     * @throws java.io.UncheckedIOException if an I/O error occurs
111     */
112    public void discardDataLine() {
113        parser.discardWithLineContinuation(
114                ObjConstants.LINE_CONTINUATION_CHAR,
115                SimpleTextParser::isNotNewLinePart)
116            .discardNewLineSequence();
117    }
118
119    /** Read a whitespace-delimited 3D vector from the current data line.
120     * @return vector vector read from the current line
121     * @throws IllegalStateException if parsing fails
122     * @throws java.io.UncheckedIOException if an I/O error occurs
123     */
124    public Vector3D readVector() {
125        discardDataLineWhitespace();
126        final double x = nextDouble();
127
128        discardDataLineWhitespace();
129        final double y = nextDouble();
130
131        discardDataLineWhitespace();
132        final double z = nextDouble();
133
134        return Vector3D.of(x, y, z);
135    }
136
137    /** Read whitespace-delimited double values from the current data line.
138     * @return double values read from the current line
139     * @throws IllegalStateException if double values are not able to be parsed
140     * @throws java.io.UncheckedIOException if an I/O error occurs
141     */
142    public double[] readDoubles() {
143        final List<Double> list = new ArrayList<>();
144
145        while (nextDataLineContent()) {
146            list.add(nextDouble());
147        }
148
149        // convert to primitive array
150        final double[] arr = new double[list.size()];
151        for (int i = 0; i < list.size(); ++i) {
152            arr[i] = list.get(i);
153        }
154
155        return arr;
156    }
157
158    /** Get the text parser for the instance.
159     * @return text parser for the instance
160     */
161    protected SimpleTextParser getTextParser() {
162        return parser;
163    }
164
165    /** Method called when a keyword is encountered in the parsed OBJ content. Subclasses should use
166     * this method to validate the keyword and/or update any internal state.
167     * @param keyword keyword encountered in the OBJ content
168     * @throws IllegalStateException if the given keyword is invalid
169     * @throws java.io.UncheckedIOException if an I/O error occurs
170     */
171    protected abstract void handleKeyword(String keyword);
172
173    /** Discard whitespace on the current data line, taking line continuation characters into account.
174     * @return text parser instance
175     * @throws java.io.UncheckedIOException if an I/O error occurs
176     */
177    protected SimpleTextParser discardDataLineWhitespace() {
178        return parser.discardWithLineContinuation(
179                ObjConstants.LINE_CONTINUATION_CHAR,
180                SimpleTextParser::isLineWhitespace);
181    }
182
183    /** Discard whitespace on the current data line and return true if any more characters
184     * remain on the line.
185     * @return true if more non-whitespace characters remain on the current data line
186     * @throws java.io.UncheckedIOException if an I/O error occurs
187     */
188    protected boolean nextDataLineContent() {
189        return discardDataLineWhitespace().hasMoreCharactersOnLine();
190    }
191
192    /** Get the next whitespace-delimited double on the current data line.
193     * @return the next whitespace-delimited double on the current line
194     * @throws IllegalStateException if a double value is not able to be parsed
195     * @throws java.io.UncheckedIOException if an I/O error occurs
196     */
197    protected double nextDouble() {
198        return parser.nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR,
199                SimpleTextParser::isNotWhitespace)
200            .getCurrentTokenAsDouble();
201    }
202
203    /** Read a keyword consisting of alphanumeric characters from the current parser position and set it
204     * as the current token. Returns true if a non-empty keyword was found.
205     * @return true if a non-empty keyword was found.
206     * @throws java.io.UncheckedIOException if an I/O error occurs
207     */
208    private boolean readKeyword() {
209        return parser
210                .nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isAlphanumeric)
211                .hasNonEmptyToken();
212    }
213}