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}