001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.json5l;
018
019import java.io.*;
020
021import org.apache.juneau.commons.reflect.*;
022import org.apache.juneau.marshall.*;
023import org.apache.juneau.marshall.json.*;
024import org.apache.juneau.marshall.json5.*;
025import org.apache.juneau.marshall.parser.*;
026import org.apache.juneau.marshall.stream.*;
027
028/**
029 * Session object that lives for the duration of a single use of {@link Json5lParser}.
030 *
031 * <p>
032 * Extends {@link Json5ParserSession} to inherit the full JSON5 dialect (single-quoted strings,
033 * bare/unquoted field names, trailing commas, missing values, comments, relaxed numbers) and
034 * re-adds JSONL's newline-delimited framing: input is read one line at a time, blank and
035 * comment-only lines are skipped, and each remaining line is parsed as one JSON5 value.
036 *
037 * <h5 class='section'>Notes:</h5><ul>
038 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
039 * </ul>
040 *
041 * <h5 class='section'>See Also:</h5><ul>
042 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Json5l">JSON5L Basics</a>
043 * </ul>
044 */
045@SuppressWarnings({
046   "java:S110", // Inheritance depth acceptable
047   "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
048   "unchecked" // Type erasure: elementType is ClassMeta<?>; toArray/convertToType return Object
049})
050public class Json5lParserSession extends Json5ParserSession {
051
052   /**
053    * Builder class.
054    */
055   public static class Builder extends Json5ParserSession.Builder {
056
057      /**
058       * Constructor
059       *
060       * @param ctx The context creating this session.
061       */
062      protected Builder(Json5lParser ctx) {
063         super(ctx);
064      }
065
066      @Override
067      public Json5lParserSession build() {
068         return new Json5lParserSession(this);
069      }
070   }
071
072   /**
073    * Creates a new builder for this object.
074    *
075    * @param ctx The context creating this session.
076    * @return A new builder.
077    */
078   public static Builder create(Json5lParser ctx) {
079      return new Builder(ctx);
080   }
081
082   /**
083    * Constructor.
084    *
085    * @param builder The builder for this object.
086    */
087   protected Json5lParserSession(Builder builder) {
088      super(builder);
089   }
090
091   /**
092    * Opens a low-level pull-parser cursor over a JSON5L document, bound to this live session.
093    *
094    * <p>
095    * Each top-level JSON5 value (one per line) is emitted as a flat sequence at depth 0; there is
096    * no virtual root container.  Same honored/ignored builder properties as
097    * {@link Json5ParserSession#readTokens(Object)}.
098    *
099    * @param input The input.  Accepts {@link Reader}, {@link CharSequence}, {@link InputStream},
100    *    or <code><jk>byte</jk>[]</code>.
101    * @return A new {@link Json5lTokenReader}.
102    * @throws IOException If a problem occurred opening the underlying input.
103    */
104   @SuppressWarnings({
105      "java:S2095" // ParserPipe lifecycle is transferred to the returned Json5lTokenReader, which closes it via its own close(); the caller owns the cursor via try-with-resources.
106   })
107   @Override /* Json5ParserSession */
108   public TokenReader readTokens(Object input) throws IOException {
109      var pipe = new ParserPipe(
110         input,
111         isDebug(),
112         false /* strict */,
113         isAutoCloseStreams(),
114         isUnbuffered(),
115         getStreamCharset());
116      return new Json5lTokenReader(pipe, new JsonTokenReader.Settings(isTrimStrings()), this);
117   }
118
119   /**
120    * Streaming array-element {@link RecordReader} for JSON5L.
121    *
122    * <p>
123    * Aliases the JSON5L line record stream: the {@link Json5lTokenReader} returned by
124    * {@link #readTokens(Object)} is itself a {@link RecordReader} that yields one record per
125    * top-level line at depth 0.  Memory is O(1) in the number of lines.
126    *
127    * @param input The input.
128    * @return A new line-delimited {@link RecordReader}.
129    * @throws IOException If a problem occurred opening the underlying input.
130    */
131   @Override /* ArrayRecordReadable */
132   public RecordReader readArrayRecords(Object input) throws IOException {
133      return readTokens(input);
134   }
135
136   @Override /* Overridden from JsonParserSession */
137   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
138      try (var r = pipe.getParserReader()) {
139         if (r == null)
140            return null;
141
142         if (type.isCollectionOrArray()) {
143            var elementType = type.getElementType();
144            var results = newGenericList();
145            var br = new BufferedReader(r);
146            String line;
147            while ((line = br.readLine()) != null) {
148               var trimmed = line.trim();
149               if (isParseable(trimmed)) {
150                  try (var linePipe = createPipe(trimmed)) {
151                     var item = super.doRead(linePipe, elementType);
152                     results.add(item);
153                  }
154               }
155            }
156            return type.isArray() ? (T) toArray(type, results) : (T) convertToType(results, type);
157         }
158
159         // Single object: parse just the first non-empty, non-comment line
160         var br = new BufferedReader(r);
161         String line;
162         while ((line = br.readLine()) != null) {
163            var trimmed = line.trim();
164            if (isParseable(trimmed)) {
165               try (var linePipe = createPipe(trimmed)) {
166                  return super.doRead(linePipe, type);
167               }
168            }
169         }
170         return null;
171      }
172   }
173
174   /**
175    * Returns <jk>true</jk> if the trimmed line contains a JSON5 value to parse — i.e. it is neither
176    * blank nor a comment-only line (a line consisting solely of a <c>//</c> line comment or a
177    * single-line <c>/* &#42;/</c> block comment).
178    *
179    * @param trimmed The whitespace-trimmed line.
180    * @return <jk>true</jk> if the line should be parsed as a JSON5 value.
181    */
182   private static boolean isParseable(String trimmed) {
183      if (trimmed.isEmpty())
184         return false;
185      if (trimmed.startsWith("//"))
186         return false;
187      // A line that is exactly a single block comment (possibly with surrounding whitespace,
188      // already trimmed) is comment-only.  Anything after the closing */ makes it parseable.
189      return !(trimmed.startsWith("/*") && trimmed.endsWith("*/") && trimmed.indexOf("*/") == trimmed.length() - 2);
190   }
191}