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>/* */</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}