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.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.util.*; 023 024/** 025 * Serializes nested objects and arrays to CSV cell inline notation: 026 * {@code {key:val;key2:val2}} and {@code [val1;val2;val3]}. 027 * 028 * <p> 029 * Used when {@code allowNestedStructures()} is enabled on the serializer. 030 */ 031public final class CsvCellSerializer { 032 033 private final CsvByteArrayCellFormat byteArrayFormat; 034 private final String nullMarker; 035 036 /** 037 * Creates a new serializer. 038 * 039 * @param byteArrayFormat Format for byte[] values. 040 * <br>Can be <jk>null</jk> (defaults to {@link CsvByteArrayCellFormat#BASE64}). 041 * @param nullMarker String to use for null values. 042 * <br>Can be <jk>null</jk> (defaults to {@code null}). 043 */ 044 public CsvCellSerializer(CsvByteArrayCellFormat byteArrayFormat, String nullMarker) { 045 this.byteArrayFormat = or(byteArrayFormat, CsvByteArrayCellFormat.BASE64); 046 this.nullMarker = or(nullMarker, "null"); 047 } 048 049 /** 050 * Serializes a value to inline notation. 051 * 052 * @param value The value to serialize. 053 * <br>Can be <jk>null</jk> (serialized as the null marker). 054 * @param session The serializer session (for bean conversion, date formatting, etc.). 055 * <br>Must not be <jk>null</jk>. 056 * @return The serialized string. 057 */ 058 public String write(Object value, CsvSerializerSession session) { 059 return writeValue(value, session); 060 } 061 062 @SuppressWarnings({ 063 "java:S3776" // Cognitive complexity acceptable for exhaustive CSV value type dispatch 064 }) 065 private String writeValue(Object value, CsvSerializerSession session) { 066 if (value == null) 067 return nullMarker; 068 if (value instanceof Map<?,?> value2) 069 return writeMap(value2, session); 070 if (value instanceof Collection<?> value2) 071 return writeCollection(value2, session); 072 if (value instanceof Object[] value2) 073 return writeObjectArray(value2, session); 074 if (value instanceof byte[] value2) 075 return byteArrayFormat == CsvByteArrayCellFormat.SEMICOLON_DELIMITED ? formatByteArraySemicolon(value2) : base64Encode(value2); 076 if (value instanceof int[] value2) return formatIntArray(value2); 077 if (value instanceof long[] value2) return formatLongArray(value2); 078 if (value instanceof double[] value2) return formatDoubleArray(value2); 079 if (value instanceof float[] value2) return formatFloatArray(value2); 080 if (value instanceof short[] value2) return formatShortArray(value2); 081 if (value instanceof boolean[] value2) return formatBooleanArray(value2); 082 if (value instanceof char[] value2) return formatCharArray(value2); 083 // Bean or simple: use session to convert/format 084 var prepared = session.prepareForInlineValue(value); 085 if (prepared instanceof Map) return writeMap((Map<?,?>) prepared, session); 086 if (prepared instanceof Collection) return writeCollection((Collection<?>) prepared, session); 087 if (prepared instanceof Object[] prepared2) return writeObjectArray(prepared2, session); 088 return escapeIfNeeded(prepared.toString()); 089 } 090 091 private String writeMap(Map<?,?> m, CsvSerializerSession session) { 092 var sb = new StringBuilder(); 093 sb.append('{'); 094 var first = true; 095 for (var e : m.entrySet()) { 096 if (!first) sb.append(';'); 097 first = false; 098 var k = e.getKey(); 099 var v = e.getValue(); 100 sb.append(escapeIfNeeded(k != null ? k.toString() : nullMarker)); 101 sb.append(':'); 102 sb.append(writeValue(v, session)); 103 } 104 sb.append('}'); 105 return sb.toString(); 106 } 107 108 private String writeCollection(Collection<?> c, CsvSerializerSession session) { 109 var sb = new StringBuilder(); 110 sb.append('['); 111 var first = true; 112 for (var v : c) { 113 if (!first) sb.append(';'); 114 first = false; 115 sb.append(writeValue(v, session)); 116 } 117 sb.append(']'); 118 return sb.toString(); 119 } 120 121 private String writeObjectArray(Object[] a, CsvSerializerSession session) { 122 var sb = new StringBuilder(); 123 sb.append('['); 124 for (var i = 0; i < a.length; i++) { 125 if (i > 0) sb.append(';'); 126 sb.append(writeValue(a[i], session)); 127 } 128 sb.append(']'); 129 return sb.toString(); 130 } 131 132 private static String formatIntArray(int[] a) { 133 var sb = new StringBuilder(); 134 sb.append('['); 135 for (var i = 0; i < a.length; i++) { 136 if (i > 0) sb.append(';'); 137 sb.append(a[i]); 138 } 139 sb.append(']'); 140 return sb.toString(); 141 } 142 143 private static String formatLongArray(long[] a) { 144 var sb = new StringBuilder(); 145 sb.append('['); 146 for (var i = 0; i < a.length; i++) { 147 if (i > 0) sb.append(';'); 148 sb.append(a[i]); 149 } 150 sb.append(']'); 151 return sb.toString(); 152 } 153 154 private static String formatDoubleArray(double[] a) { 155 var sb = new StringBuilder(); 156 sb.append('['); 157 for (var i = 0; i < a.length; i++) { 158 if (i > 0) sb.append(';'); 159 sb.append(a[i]); 160 } 161 sb.append(']'); 162 return sb.toString(); 163 } 164 165 private static String formatFloatArray(float[] a) { 166 var sb = new StringBuilder(); 167 sb.append('['); 168 for (var i = 0; i < a.length; i++) { 169 if (i > 0) sb.append(';'); 170 sb.append(a[i]); 171 } 172 sb.append(']'); 173 return sb.toString(); 174 } 175 176 private static String formatShortArray(short[] a) { 177 var sb = new StringBuilder(); 178 sb.append('['); 179 for (var i = 0; i < a.length; i++) { 180 if (i > 0) sb.append(';'); 181 sb.append(a[i]); 182 } 183 sb.append(']'); 184 return sb.toString(); 185 } 186 187 private static String formatBooleanArray(boolean[] a) { 188 var sb = new StringBuilder(); 189 sb.append('['); 190 for (var i = 0; i < a.length; i++) { 191 if (i > 0) sb.append(';'); 192 sb.append(a[i]); 193 } 194 sb.append(']'); 195 return sb.toString(); 196 } 197 198 private static String formatCharArray(char[] a) { 199 var sb = new StringBuilder(); 200 sb.append('['); 201 for (var i = 0; i < a.length; i++) { 202 if (i > 0) sb.append(';'); 203 sb.append((int) a[i]); 204 } 205 sb.append(']'); 206 return sb.toString(); 207 } 208 209 private static String formatByteArraySemicolon(byte[] b) { 210 var sb = new StringBuilder(); 211 for (var i = 0; i < b.length; i++) { 212 if (i > 0) sb.append(';'); 213 sb.append(b[i] & 0xff); 214 } 215 return sb.toString(); 216 } 217 218 private static String escapeIfNeeded(String s) { 219 if (s == null) 220 return ""; 221 for (var i = 0; i < s.length(); i++) { 222 var c = s.charAt(i); 223 if (c == ';' || c == ':' || c == '{' || c == '}' || c == '[' || c == ']' || c == '"' || c == '\\') 224 return quoted(s); 225 } 226 return s; 227 } 228 229 private static String quoted(String s) { 230 var sb = new StringBuilder(); 231 sb.append('"'); 232 for (var i = 0; i < s.length(); i++) { 233 var c = s.charAt(i); 234 if (c == '"' || c == '\\') 235 sb.append('\\'); 236 sb.append(c); 237 } 238 sb.append('"'); 239 return sb.toString(); 240 } 241}