1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.geometry.io.euclidean.threed.obj; 18 19 import java.util.ArrayList; 20 import java.util.List; 21 22 import org.apache.commons.geometry.euclidean.threed.Vector3D; 23 import org.apache.commons.geometry.io.core.internal.SimpleTextParser; 24 25 /** Abstract base class for OBJ parsing functionality. 26 */ 27 public abstract class AbstractObjParser { 28 29 /** Text parser instance. */ 30 private final SimpleTextParser parser; 31 32 /** The current (most recently parsed) keyword. */ 33 private String currentKeyword; 34 35 /** Construct a new instance for parsing OBJ content from the given text parser. 36 * @param parser text parser to read content from 37 */ 38 protected AbstractObjParser(final SimpleTextParser parser) { 39 this.parser = parser; 40 } 41 42 /** Get the current keyword, meaning the keyword most recently parsed via the {@link #nextKeyword()} 43 * method. Null is returned if parsing has not started or the end of the content has been reached. 44 * @return the current keyword or null if parsing has not started or the end 45 * of the content has been reached 46 */ 47 public String getCurrentKeyword() { 48 return currentKeyword; 49 } 50 51 /** Advance the parser to the next keyword, returning true if a keyword has been found 52 * and false if the end of the content has been reached. Keywords consist of alphanumeric 53 * strings placed at the beginning of lines. Comments and blank lines are ignored. 54 * @return true if a keyword has been found and false if the end of content has been reached 55 * @throws IllegalStateException if invalid content is found 56 * @throws java.io.UncheckedIOException if an I/O error occurs 57 */ 58 public boolean nextKeyword() { 59 currentKeyword = null; 60 61 // advance to the next line if not at the start of a line 62 if (parser.getColumnNumber() != 1) { 63 discardDataLine(); 64 } 65 66 // search for the next keyword 67 while (currentKeyword == null && parser.hasMoreCharacters()) { 68 if (!nextDataLineContent() || 69 parser.peekChar() == ObjConstants.COMMENT_CHAR) { 70 // use a standard line discard here so we don't interpret line continuations 71 // within comments; the interpreted OBJ content should be the same regardless 72 // of the presence of comments 73 parser.discardLine(); 74 } else if (parser.getColumnNumber() != 1) { 75 throw parser.parseError("non-blank lines must begin with an OBJ keyword or comment character"); 76 } else if (!readKeyword()) { 77 throw parser.unexpectedToken("OBJ keyword"); 78 } else { 79 final String keywordValue = parser.getCurrentToken(); 80 81 handleKeyword(keywordValue); 82 83 currentKeyword = keywordValue; 84 85 // advance past whitespace to the next data value 86 discardDataLineWhitespace(); 87 } 88 } 89 90 return currentKeyword != null; 91 } 92 93 /** Read the remaining content on the current data line, taking line continuation characters into 94 * account. 95 * @return remaining content on the current data line or null if the end of the content has 96 * been reached 97 * @throws java.io.UncheckedIOException if an I/O error occurs 98 */ 99 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 }