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.csv; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022 023import org.apache.juneau.marshall.parser.*; 024 025/** 026 * Parses CSV cell inline notation: {@code {key:val;key2:val2}} and {@code [val1;val2;val3]}. 027 * 028 * <p> 029 * Grammar (CSV-specific, semicolon-separated): 030 * <ul> 031 * <li>Object: {@code \{ (key ':' value (';' key ':' value)*)? \}} 032 * <li>Array: {@code \[ (value (';' value)*)? \]} 033 * <li>Values: object | array | quoted string | number | identifier | null 034 * </ul> 035 * 036 * <h5 class='section'>Notes:</h5><ul> 037 * <li class='note'>Reserved characters: {@code ; : \{ \} [ ] " \} 038 * </ul> 039 */ 040public final class CsvCellParser { 041 042 private final String input; 043 private final String nullMarker; 044 private int pos; 045 private final int length; 046 047 private CsvCellParser(String input, String nullMarker) { 048 this.input = input != null ? input.trim() : ""; 049 this.nullMarker = or(nullMarker, "null"); 050 this.length = this.input.length(); 051 } 052 053 /** 054 * Parses a cell value string into a Map, List, or scalar. 055 * 056 * @param cell The cell string (e.g. {@code {a:1;b:2}} or {@code [1;2;3]}). 057 * <br>Can be <jk>null</jk> (returns <jk>null</jk>). 058 * @param nullMarker The string that denotes null (e.g. {@code <NULL>} or {@code null}). 059 * <br>Can be <jk>null</jk> (defaults to {@code null}). 060 * @return Parsed object: Map, List, String, Number, Boolean, or null. 061 * @throws ParseException If the input is invalid. 062 */ 063 public static Object read(String cell, String nullMarker) throws ParseException { 064 if (ie(cell)) 065 return null; 066 var p = new CsvCellParser(cell, nullMarker); 067 try { 068 return p.readValue(); 069 } catch (Exception e) { 070 throw new ParseException(e, "Invalid CSV cell notation at position %s: '%s'", p.pos, cell); 071 } 072 } 073 074 private void skipWhitespace() { 075 while (pos < length && Character.isWhitespace(input.charAt(pos))) 076 pos++; 077 } 078 079 private boolean hasMore() { 080 skipWhitespace(); 081 return pos < length; 082 } 083 084 private char peek() { 085 return pos < length ? input.charAt(pos) : '\0'; 086 } 087 088 private char consume() { 089 return pos < length ? input.charAt(pos++) : '\0'; 090 } 091 092 private Object readValue() throws ParseException { 093 skipWhitespace(); 094 if (pos >= length) 095 return null; 096 var c = peek(); 097 if (c == '{') 098 return readObject(); 099 if (c == '[') 100 return readArray(); 101 if (c == '"') 102 return readQuotedString(); 103 return readSimpleValue(); 104 } 105 106 private Map<String,Object> readObject() throws ParseException { 107 if (consume() != '{') 108 throw new ParseException("Expected opening brace"); 109 var m = new LinkedHashMap<String,Object>(); 110 skipWhitespace(); 111 if (peek() == '}') { 112 consume(); 113 return m; 114 } 115 while (hasMore()) { 116 var key = readKey(); 117 skipWhitespace(); 118 if (consume() != ':') 119 throw new ParseException("Expected ':' after key at position " + pos); 120 var value = readValue(); 121 m.put(key, value); 122 skipWhitespace(); 123 var c = peek(); 124 if (c == '}') { 125 consume(); 126 break; 127 } 128 if (c != ';') 129 throw new ParseException("Expected ';' or '}' at position " + pos); 130 consume(); // skip ';' 131 } 132 return m; 133 } 134 135 private String readKey() throws ParseException { 136 skipWhitespace(); 137 if (pos >= length) 138 throw new ParseException("Unexpected end of input in key"); 139 if (peek() == '"') 140 return readQuotedString(); 141 return readIdentifier(); 142 } 143 144 private List<Object> readArray() throws ParseException { 145 if (consume() != '[') 146 throw new ParseException("Expected opening bracket"); 147 var list = l(); 148 skipWhitespace(); 149 if (peek() == ']') { 150 consume(); 151 return list; 152 } 153 while (hasMore()) { 154 list.add(readValue()); 155 skipWhitespace(); 156 var c = peek(); 157 if (c == ']') { 158 consume(); 159 break; 160 } 161 if (c != ';') 162 throw new ParseException("Expected ';' or ']' at position " + pos); 163 consume(); // skip ';' 164 } 165 return list; 166 } 167 168 private String readQuotedString() throws ParseException { 169 if (consume() != '"') 170 throw new ParseException("Expected quote"); 171 var sb = new StringBuilder(); 172 while (pos < length) { 173 var c = consume(); 174 if (c == '"') 175 return sb.toString(); 176 if (c == '\\' && pos < length) 177 sb.append(consume()); // escaped char 178 else 179 sb.append(c); 180 } 181 throw new ParseException("Unterminated quoted string"); 182 } 183 184 private String readIdentifier() { 185 var start = pos; 186 while (pos < length) { 187 var c = input.charAt(pos); 188 if (c == ';' || c == ':' || c == '{' || c == '}' || c == '[' || c == ']' || c == '"' || Character.isWhitespace(c)) 189 break; 190 pos++; 191 } 192 return input.substring(start, pos); 193 } 194 195 private Object readSimpleValue() throws ParseException { 196 skipWhitespace(); 197 if (pos >= length || peek() == ';' || peek() == '}' || peek() == ']') 198 return ""; 199 var id = readIdentifier(); 200 if (id.isEmpty()) 201 throw new ParseException("Expected value at position " + pos); 202 if (eqic(id, nullMarker)) 203 return null; 204 if (eq(id, "true")) 205 return Boolean.TRUE; 206 if (eq(id, "false")) 207 return Boolean.FALSE; 208 // Try number 209 try { 210 if (id.contains(".")) { 211 var d = Double.parseDouble(id); 212 if (d == (long) d) 213 return Long.valueOf((long) d); 214 return Double.valueOf(d); 215 } 216 return Long.valueOf(id); 217 } catch (@SuppressWarnings("unused") NumberFormatException e) { 218 // Not a number, treat as string 219 return id; 220 } 221 } 222}