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}