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 }