AbstractObjParser.java

  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. import java.util.ArrayList;
  19. import java.util.List;

  20. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  21. import org.apache.commons.geometry.io.core.internal.SimpleTextParser;

  22. /** Abstract base class for OBJ parsing functionality.
  23.  */
  24. public abstract class AbstractObjParser {

  25.     /** Text parser instance. */
  26.     private final SimpleTextParser parser;

  27.     /** The current (most recently parsed) keyword. */
  28.     private String currentKeyword;

  29.     /** Construct a new instance for parsing OBJ content from the given text parser.
  30.      * @param parser text parser to read content from
  31.      */
  32.     protected AbstractObjParser(final SimpleTextParser parser) {
  33.         this.parser = parser;
  34.     }

  35.     /** Get the current keyword, meaning the keyword most recently parsed via the {@link #nextKeyword()}
  36.      * method. Null is returned if parsing has not started or the end of the content has been reached.
  37.      * @return the current keyword or null if parsing has not started or the end
  38.      *      of the content has been reached
  39.      */
  40.     public String getCurrentKeyword() {
  41.         return currentKeyword;
  42.     }

  43.     /** Advance the parser to the next keyword, returning true if a keyword has been found
  44.      * and false if the end of the content has been reached. Keywords consist of alphanumeric
  45.      * strings placed at the beginning of lines. Comments and blank lines are ignored.
  46.      * @return true if a keyword has been found and false if the end of content has been reached
  47.      * @throws IllegalStateException if invalid content is found
  48.      * @throws java.io.UncheckedIOException if an I/O error occurs
  49.      */
  50.     public boolean nextKeyword() {
  51.         currentKeyword = null;

  52.         // advance to the next line if not at the start of a line
  53.         if (parser.getColumnNumber() != 1) {
  54.             discardDataLine();
  55.         }

  56.         // search for the next keyword
  57.         while (currentKeyword == null && parser.hasMoreCharacters()) {
  58.             if (!nextDataLineContent() ||
  59.                     parser.peekChar() == ObjConstants.COMMENT_CHAR) {
  60.                 // use a standard line discard here so we don't interpret line continuations
  61.                 // within comments; the interpreted OBJ content should be the same regardless
  62.                 // of the presence of comments
  63.                 parser.discardLine();
  64.             } else if (parser.getColumnNumber() != 1) {
  65.                 throw parser.parseError("non-blank lines must begin with an OBJ keyword or comment character");
  66.             } else if (!readKeyword()) {
  67.                 throw parser.unexpectedToken("OBJ keyword");
  68.             } else {
  69.                 final String keywordValue = parser.getCurrentToken();

  70.                 handleKeyword(keywordValue);

  71.                 currentKeyword = keywordValue;

  72.                 // advance past whitespace to the next data value
  73.                 discardDataLineWhitespace();
  74.             }
  75.         }

  76.         return currentKeyword != null;
  77.     }

  78.     /** Read the remaining content on the current data line, taking line continuation characters into
  79.      * account.
  80.      * @return remaining content on the current data line or null if the end of the content has
  81.      *      been reached
  82.      * @throws java.io.UncheckedIOException if an I/O error occurs
  83.      */
  84.     public String readDataLine() {
  85.         parser.nextWithLineContinuation(
  86.                 ObjConstants.LINE_CONTINUATION_CHAR,
  87.                 SimpleTextParser::isNotNewLinePart)
  88.             .discardNewLineSequence();

  89.         return parser.getCurrentToken();
  90.     }

  91.     /** Discard remaining content on the current data line, taking line continuation characters into
  92.      * account.
  93.      * @throws java.io.UncheckedIOException if an I/O error occurs
  94.      */
  95.     public void discardDataLine() {
  96.         parser.discardWithLineContinuation(
  97.                 ObjConstants.LINE_CONTINUATION_CHAR,
  98.                 SimpleTextParser::isNotNewLinePart)
  99.             .discardNewLineSequence();
  100.     }

  101.     /** Read a whitespace-delimited 3D vector from the current data line.
  102.      * @return vector vector read from the current line
  103.      * @throws IllegalStateException if parsing fails
  104.      * @throws java.io.UncheckedIOException if an I/O error occurs
  105.      */
  106.     public Vector3D readVector() {
  107.         discardDataLineWhitespace();
  108.         final double x = nextDouble();

  109.         discardDataLineWhitespace();
  110.         final double y = nextDouble();

  111.         discardDataLineWhitespace();
  112.         final double z = nextDouble();

  113.         return Vector3D.of(x, y, z);
  114.     }

  115.     /** Read whitespace-delimited double values from the current data line.
  116.      * @return double values read from the current line
  117.      * @throws IllegalStateException if double values are not able to be parsed
  118.      * @throws java.io.UncheckedIOException if an I/O error occurs
  119.      */
  120.     public double[] readDoubles() {
  121.         final List<Double> list = new ArrayList<>();

  122.         while (nextDataLineContent()) {
  123.             list.add(nextDouble());
  124.         }

  125.         // convert to primitive array
  126.         final double[] arr = new double[list.size()];
  127.         for (int i = 0; i < list.size(); ++i) {
  128.             arr[i] = list.get(i);
  129.         }

  130.         return arr;
  131.     }

  132.     /** Get the text parser for the instance.
  133.      * @return text parser for the instance
  134.      */
  135.     protected SimpleTextParser getTextParser() {
  136.         return parser;
  137.     }

  138.     /** Method called when a keyword is encountered in the parsed OBJ content. Subclasses should use
  139.      * this method to validate the keyword and/or update any internal state.
  140.      * @param keyword keyword encountered in the OBJ content
  141.      * @throws IllegalStateException if the given keyword is invalid
  142.      * @throws java.io.UncheckedIOException if an I/O error occurs
  143.      */
  144.     protected abstract void handleKeyword(String keyword);

  145.     /** Discard whitespace on the current data line, taking line continuation characters into account.
  146.      * @return text parser instance
  147.      * @throws java.io.UncheckedIOException if an I/O error occurs
  148.      */
  149.     protected SimpleTextParser discardDataLineWhitespace() {
  150.         return parser.discardWithLineContinuation(
  151.                 ObjConstants.LINE_CONTINUATION_CHAR,
  152.                 SimpleTextParser::isLineWhitespace);
  153.     }

  154.     /** Discard whitespace on the current data line and return true if any more characters
  155.      * remain on the line.
  156.      * @return true if more non-whitespace characters remain on the current data line
  157.      * @throws java.io.UncheckedIOException if an I/O error occurs
  158.      */
  159.     protected boolean nextDataLineContent() {
  160.         return discardDataLineWhitespace().hasMoreCharactersOnLine();
  161.     }

  162.     /** Get the next whitespace-delimited double on the current data line.
  163.      * @return the next whitespace-delimited double on the current line
  164.      * @throws IllegalStateException if a double value is not able to be parsed
  165.      * @throws java.io.UncheckedIOException if an I/O error occurs
  166.      */
  167.     protected double nextDouble() {
  168.         return parser.nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR,
  169.                 SimpleTextParser::isNotWhitespace)
  170.             .getCurrentTokenAsDouble();
  171.     }

  172.     /** Read a keyword consisting of alphanumeric characters from the current parser position and set it
  173.      * as the current token. Returns true if a non-empty keyword was found.
  174.      * @return true if a non-empty keyword was found.
  175.      * @throws java.io.UncheckedIOException if an I/O error occurs
  176.      */
  177.     private boolean readKeyword() {
  178.         return parser
  179.                 .nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isAlphanumeric)
  180.                 .hasNonEmptyToken();
  181.     }
  182. }