View Javadoc
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 }