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 }