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.jsonl;
018
019
020import java.io.*;
021
022import org.apache.juneau.commons.reflect.*;
023import org.apache.juneau.marshall.*;
024import org.apache.juneau.marshall.json.*;
025import org.apache.juneau.marshall.parser.*;
026import org.apache.juneau.marshall.stream.*;
027import static org.apache.juneau.commons.utils.Shorts.*;
028
029/**
030 * Session object that lives for the duration of a single use of {@link JsonlParser}.
031 *
032 * <h5 class='section'>Notes:</h5><ul>
033 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
034 * </ul>
035 *
036 * <h5 class='section'>See Also:</h5><ul>
037 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonlSupport">JSONL</a>
038 * </ul>
039 */
040@SuppressWarnings({
041   "java:S110", // Inheritance depth acceptable
042   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
043   "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
044   "unchecked" // Type erasure: elementType is ClassMeta<?>; toArray/convertToType return Object
045})
046public class JsonlParserSession extends JsonParserSession {
047
048   /**
049    * Builder class.
050    */
051   @SuppressWarnings({
052      "unused" // ctx passed to super(); parent uses it; Builder stores for constructor chain
053   })
054   public static class Builder extends JsonParserSession.Builder<Builder> {
055
056      private final JsonlParser ctx;
057
058      /**
059       * Constructor
060       *
061       * @param ctx The context creating this session.
062       *    <br>Cannot be <jk>null</jk>.
063       */
064      protected Builder(JsonlParser ctx) {
065         super(reqnn("ctx", ctx));
066         this.ctx = ctx;
067      }
068
069      @Override
070      public JsonlParserSession build() {
071         return new JsonlParserSession(this);
072      }
073   }
074
075   /**
076    * Creates a new builder for this object.
077    *
078    * @param ctx The context creating this session.
079    *    <br>Cannot be <jk>null</jk>.
080    * @return A new builder.
081    */
082   public static Builder create(JsonlParser ctx) {
083      return new Builder(reqnn("ctx", ctx));
084   }
085
086   /**
087    * Constructor.
088    *
089    * @param builder The builder for this object.
090    */
091   protected JsonlParserSession(Builder builder) {
092      super(builder);
093   }
094
095   /**
096    * Opens a low-level pull-parser cursor over a JSONL document, bound to this live session.
097    *
098    * <p>
099    * Each top-level JSON value (one per line) is emitted as a flat sequence at depth 0; there
100    * is no virtual root container.  See {@link JsonlTokenReader} for the contract.  Same
101    * honored/ignored builder properties as {@link JsonParserSession#readTokens(Object)}.
102    *
103    * @param input The input.  Accepts {@link Reader}, {@link CharSequence},
104    *    {@link InputStream}, or <code><jk>byte</jk>[]</code>.
105    * @return A new {@link JsonlTokenReader}.
106    * @throws IOException If a problem occurred opening the underlying input.
107    */
108   @SuppressWarnings({
109      "java:S2095" // ParserPipe lifecycle is transferred to the returned JsonlTokenReader, which closes it via its own close(); the caller owns the cursor via try-with-resources.
110   })
111   @Override /* JsonParserSession */
112   public TokenReader readTokens(Object input) throws IOException {
113      var pipe = new ParserPipe(
114         input,
115         isDebug(),
116         false /* strict */,
117         isAutoCloseStreams(),
118         isUnbuffered(),
119         getStreamCharset());
120      return new JsonlTokenReader(pipe, new JsonTokenReader.Settings(isTrimStrings()), this);
121   }
122
123   /**
124    * Streaming array-element {@link RecordReader} for JSONL.
125    *
126    * <p>
127    * Unlike the inherited {@link JsonParserSession#readArrayRecords(Object)} &mdash; which expects a
128    * bracketed top-level <c>[...]</c> array and is invalid for line-delimited JSONL &mdash; this aliases
129    * the JSONL line record stream: the {@link JsonlTokenReader} returned by {@link #readTokens(Object)}
130    * is itself a {@link RecordReader} that yields one record per top-level line at depth 0.  Memory is
131    * O(1) in the number of lines.
132    *
133    * @param input The input.
134    * @return A new line-delimited {@link RecordReader}.
135    * @throws IOException If a problem occurred opening the underlying input.
136    */
137   @Override /* ArrayRecordReadable */
138   public RecordReader readArrayRecords(Object input) throws IOException {
139      return readTokens(input);
140   }
141
142   @Override /* Overridden from JsonParserSession */
143   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
144      try (var r = pipe.getParserReader()) {
145         if (r == null)
146            return null;
147
148         if (type.isCollectionOrArray()) {
149            var elementType = type.getElementType();
150            var results = newGenericList();
151            var br = new BufferedReader(r);
152            String line;
153            while ((line = br.readLine()) != null) {
154               var trimmed = line.trim();
155               if (!trimmed.isEmpty()) {
156                  try (var linePipe = createPipe(trimmed)) {
157                     var item = super.doRead(linePipe, elementType);
158                     results.add(item);
159                  }
160               }
161            }
162            return type.isArray() ? (T) toArray(type, results) : (T) convertToType(results, type);
163         }
164
165         // Single object: parse just the first non-empty line
166         var br = new BufferedReader(r);
167         String line;
168         while ((line = br.readLine()) != null) {
169            var trimmed = line.trim();
170            if (!trimmed.isEmpty()) {
171               try (var linePipe = createPipe(trimmed)) {
172                  return super.doRead(linePipe, type);
173               }
174            }
175         }
176         return null;
177      }
178   }
179}