AbstractObjParser.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.geometry.io.euclidean.threed.obj;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
/** Abstract base class for OBJ parsing functionality.
*/
public abstract class AbstractObjParser {
/** Text parser instance. */
private final SimpleTextParser parser;
/** The current (most recently parsed) keyword. */
private String currentKeyword;
/** Construct a new instance for parsing OBJ content from the given text parser.
* @param parser text parser to read content from
*/
protected AbstractObjParser(final SimpleTextParser parser) {
this.parser = parser;
}
/** Get the current keyword, meaning the keyword most recently parsed via the {@link #nextKeyword()}
* method. Null is returned if parsing has not started or the end of the content has been reached.
* @return the current keyword or null if parsing has not started or the end
* of the content has been reached
*/
public String getCurrentKeyword() {
return currentKeyword;
}
/** Advance the parser to the next keyword, returning true if a keyword has been found
* and false if the end of the content has been reached. Keywords consist of alphanumeric
* strings placed at the beginning of lines. Comments and blank lines are ignored.
* @return true if a keyword has been found and false if the end of content has been reached
* @throws IllegalStateException if invalid content is found
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public boolean nextKeyword() {
currentKeyword = null;
// advance to the next line if not at the start of a line
if (parser.getColumnNumber() != 1) {
discardDataLine();
}
// search for the next keyword
while (currentKeyword == null && parser.hasMoreCharacters()) {
if (!nextDataLineContent() ||
parser.peekChar() == ObjConstants.COMMENT_CHAR) {
// use a standard line discard here so we don't interpret line continuations
// within comments; the interpreted OBJ content should be the same regardless
// of the presence of comments
parser.discardLine();
} else if (parser.getColumnNumber() != 1) {
throw parser.parseError("non-blank lines must begin with an OBJ keyword or comment character");
} else if (!readKeyword()) {
throw parser.unexpectedToken("OBJ keyword");
} else {
final String keywordValue = parser.getCurrentToken();
handleKeyword(keywordValue);
currentKeyword = keywordValue;
// advance past whitespace to the next data value
discardDataLineWhitespace();
}
}
return currentKeyword != null;
}
/** Read the remaining content on the current data line, taking line continuation characters into
* account.
* @return remaining content on the current data line or null if the end of the content has
* been reached
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public String readDataLine() {
parser.nextWithLineContinuation(
ObjConstants.LINE_CONTINUATION_CHAR,
SimpleTextParser::isNotNewLinePart)
.discardNewLineSequence();
return parser.getCurrentToken();
}
/** Discard remaining content on the current data line, taking line continuation characters into
* account.
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public void discardDataLine() {
parser.discardWithLineContinuation(
ObjConstants.LINE_CONTINUATION_CHAR,
SimpleTextParser::isNotNewLinePart)
.discardNewLineSequence();
}
/** Read a whitespace-delimited 3D vector from the current data line.
* @return vector vector read from the current line
* @throws IllegalStateException if parsing fails
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public Vector3D readVector() {
discardDataLineWhitespace();
final double x = nextDouble();
discardDataLineWhitespace();
final double y = nextDouble();
discardDataLineWhitespace();
final double z = nextDouble();
return Vector3D.of(x, y, z);
}
/** Read whitespace-delimited double values from the current data line.
* @return double values read from the current line
* @throws IllegalStateException if double values are not able to be parsed
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public double[] readDoubles() {
final List<Double> list = new ArrayList<>();
while (nextDataLineContent()) {
list.add(nextDouble());
}
// convert to primitive array
final double[] arr = new double[list.size()];
for (int i = 0; i < list.size(); ++i) {
arr[i] = list.get(i);
}
return arr;
}
/** Get the text parser for the instance.
* @return text parser for the instance
*/
protected SimpleTextParser getTextParser() {
return parser;
}
/** Method called when a keyword is encountered in the parsed OBJ content. Subclasses should use
* this method to validate the keyword and/or update any internal state.
* @param keyword keyword encountered in the OBJ content
* @throws IllegalStateException if the given keyword is invalid
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
protected abstract void handleKeyword(String keyword);
/** Discard whitespace on the current data line, taking line continuation characters into account.
* @return text parser instance
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
protected SimpleTextParser discardDataLineWhitespace() {
return parser.discardWithLineContinuation(
ObjConstants.LINE_CONTINUATION_CHAR,
SimpleTextParser::isLineWhitespace);
}
/** Discard whitespace on the current data line and return true if any more characters
* remain on the line.
* @return true if more non-whitespace characters remain on the current data line
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
protected boolean nextDataLineContent() {
return discardDataLineWhitespace().hasMoreCharactersOnLine();
}
/** Get the next whitespace-delimited double on the current data line.
* @return the next whitespace-delimited double on the current line
* @throws IllegalStateException if a double value is not able to be parsed
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
protected double nextDouble() {
return parser.nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR,
SimpleTextParser::isNotWhitespace)
.getCurrentTokenAsDouble();
}
/** Read a keyword consisting of alphanumeric characters from the current parser position and set it
* as the current token. Returns true if a non-empty keyword was found.
* @return true if a non-empty keyword was found.
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private boolean readKeyword() {
return parser
.nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isAlphanumeric)
.hasNonEmptyToken();
}
}