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.hjson; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022 023import org.apache.juneau.commons.bean.*; 024import org.apache.juneau.marshall.*; 025import org.apache.juneau.marshall.collections.*; 026import org.apache.juneau.marshall.parser.*; 027import org.apache.juneau.marshall.stream.*; 028 029/** 030 * Parses Hjson (Human JSON) text into POJO models. 031 * 032 * <h5 class='topic'>Media types</h5> 033 * <p> 034 * Handles <c>Content-Type</c> types: <bc>application/hjson, application/hjson+json</bc> 035 * 036 * <h5 class='topic'>Description</h5> 037 * <p> 038 * This parser handles all standard Hjson syntax as defined by the 039 * <a class="doclink" href="https://hjson.github.io/syntax.html">Hjson specification</a>. 040 * 041 * <p> 042 * In addition to standard JSON, this parser handles the following: 043 * <ul class='spaced-list'> 044 * <li> 045 * {@code #} single-line comments (to end of line). 046 * <li> 047 * {@code //} single-line comments. 048 * <li> 049 * {@code /* ... *}{@code /} block comments. 050 * <li> 051 * Quoteless string values (text read to end of line, leading/trailing whitespace trimmed). 052 * <li> 053 * Multiline strings delimited by {@code '''} (triple single-quote) with indentation stripping. 054 * <li> 055 * Optional commas — newlines may serve as member/element separators. 056 * <li> 057 * Trailing commas (ignored). 058 * <li> 059 * Unquoted keys. 060 * <li> 061 * Root-braceless objects (top-level content without wrapping {@code {}}). 062 * </ul> 063 * 064 * <p> 065 * Value type disambiguation: the parser identifies value types in this order: 066 * number → boolean → null → quoteless string. 067 * A value such as {@code 5 times} that starts like a number but contains non-numeric 068 * characters is treated as a quoteless string. 069 * 070 * <p> 071 * This parser handles the following input and returns the corresponding Java type: 072 * <ul class='spaced-list'> 073 * <li> 074 * Hjson/JSON objects ({@code {...}}) are converted to {@link JsonMap JsonMaps}. 075 * <b>Note:</b> If a <code><xa>_type</xa>=<xs>'xxx'</xs></code> attribute is specified on the object, 076 * an attempt is made to convert the object to an instance of the specified Java bean class. 077 * <li> 078 * Hjson/JSON arrays ({@code [...]}) are converted to {@link JsonList JsonLists}. 079 * <li> 080 * Quoted strings ({@code "..."} or {@code '...'}) are converted to {@link String Strings}. 081 * <li> 082 * Multiline strings ({@code '''...'''}) are converted to {@link String Strings}. 083 * <li> 084 * Quoteless strings are converted to {@link String Strings}. 085 * <li> 086 * Numbers are converted to {@link Integer Integers}, {@link Long Longs}, 087 * {@link Float Floats}, or {@link Double Doubles} depending on size and format. 088 * <li> 089 * {@code true}/{@code false} are converted to {@link Boolean Booleans}. 090 * <li> 091 * {@code null} returns <jk>null</jk>. 092 * <li> 093 * Input consisting of only whitespace or comments returns <jk>null</jk>. 094 * </ul> 095 * 096 * <h5 class='figure'>Example input - readable mode:</h5> 097 * <p class='bjson'> 098 * { 099 * name: Alice 100 * age: 30 101 * } 102 * </p> 103 * 104 * <h5 class='figure'>Example input - compact mode:</h5> 105 * <p class='bjson'> 106 * {name:Alice,age:30} 107 * </p> 108 * 109 * <h5 class='section'>Notes:</h5><ul> 110 * <li class='note'>Root-braceless Hjson files (content without a wrapping {@code {}}) are supported on input, 111 * but {@link HjsonSerializer} always emits root braces by default. 112 * A parse-then-serialize round-trip of a root-braceless file will produce braced output. 113 * <li class='note'>This class is thread safe and reusable. 114 * </ul> 115 * 116 * <h5 class='section'>See Also:</h5><ul> 117 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Hjson">Hjson</a> 118 * <li class='link'><a class="doclink" href="https://hjson.github.io/syntax.html">Hjson Specification</a> 119 * </ul> 120 */ 121@SuppressWarnings({ 122 "java:S110", // Extends ReaderParser, which sits in the shared Context > Parser > ReaderParser hierarchy used by every character-based parser 123 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 124 "java:S9149", // Per-format static factories intentionally shadow the parent's. 125 "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 126}) 127public class HjsonParser extends ReaderParser implements HjsonMetaProvider, RecordReadable { 128 129 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HjsonClassMeta> hjsonClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 130 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HjsonBeanPropertyMeta> hjsonBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 131 132 /** 133 * Builder class. 134 */ 135 public static class Builder extends ReaderParser.Builder<Builder> { 136 137 protected Builder() { 138 consumes("application/hjson,application/hjson+json"); 139 } 140 141 protected Builder(Builder copyFrom) { 142 super(reqnn("ctx", copyFrom)); 143 } 144 145 protected Builder(HjsonParser copyFrom) { 146 super(reqnn("ctx", copyFrom)); 147 } 148 149 @Override 150 public HjsonParser build() { 151 return new HjsonParser(this); 152 } 153 154 @Override 155 public Builder copy() { 156 return new Builder(this); 157 } 158 159 } 160 161 /** Default parser. */ 162 public static final HjsonParser DEFAULT = new HjsonParser(create()); 163 164 /** 165 * Creates a new builder. 166 * 167 * @return A new builder. 168 */ 169 public static Builder create() { 170 return new Builder(); 171 } 172 173 /** 174 * Constructor. 175 * 176 * @param builder The builder. 177 */ 178 public HjsonParser(Builder builder) { 179 super(builder); 180 } 181 182 @Override 183 public HjsonParserSession.Builder createSession() { 184 return HjsonParserSession.create(this); 185 } 186 187 @Override 188 public Builder copy() { 189 return new Builder(this); 190 } 191 192 @Override 193 public HjsonBeanPropertyMeta getHjsonBeanPropertyMeta(BeanPropertyMeta bpm) { 194 if (bpm == null) 195 return HjsonBeanPropertyMeta.DEFAULT; 196 return hjsonBeanPropertyMetas.computeIfAbsent(bpm, k -> new HjsonBeanPropertyMeta(k, this)); 197 } 198 199 @Override 200 public HjsonClassMeta getHjsonClassMeta(ClassMeta<?> cm) { 201 return hjsonClassMetas.computeIfAbsent(cm, k -> new HjsonClassMeta(k, this)); 202 } 203 204 /** 205 * Convenience delegator that opens a {@link RecordReader} over the input using 206 * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}). 207 * 208 * <p> 209 * The real implementation lives on {@link HjsonParserSession#readRecords(Object)}. Callers 210 * that need request-derived configuration (locale, timezone, schema, swaps) should call 211 * {@link #createSession()} and invoke {@link HjsonParserSession#readRecords(Object)} on the 212 * built session instead. 213 * 214 * @param input The input. 215 * @return A new {@link RecordReader} cursor. 216 * @throws IOException If a problem occurred opening the underlying input. 217 */ 218 @Override /* RecordReadable */ 219 public RecordReader readRecords(Object input) throws IOException { 220 return ((RecordReadable) getSession()).readRecords(input); 221 } 222 223 @Override /* RecordReadable */ 224 public boolean isRecordStreaming() { 225 return false; 226 } 227}