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)} — which expects a 128 * bracketed top-level <c>[...]</c> array and is invalid for line-delimited JSONL — 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}