001package ezvcard.io.json;
002
003import java.io.Closeable;
004import java.io.Flushable;
005import java.io.IOException;
006import java.io.Writer;
007import java.util.List;
008import java.util.Map;
009
010import com.fasterxml.jackson.core.JsonFactory;
011import com.fasterxml.jackson.core.JsonGenerator;
012import com.fasterxml.jackson.core.JsonGenerator.Feature;
013import com.fasterxml.jackson.core.PrettyPrinter;
014
015import ezvcard.Messages;
016import ezvcard.VCardDataType;
017import ezvcard.parameter.VCardParameters;
018
019/*
020 Copyright (c) 2012-2026, Michael Angstadt
021 All rights reserved.
022
023 Redistribution and use in source and binary forms, with or without
024 modification, are permitted provided that the following conditions are met: 
025
026 1. Redistributions of source code must retain the above copyright notice, this
027 list of conditions and the following disclaimer. 
028 2. Redistributions in binary form must reproduce the above copyright notice,
029 this list of conditions and the following disclaimer in the documentation
030 and/or other materials provided with the distribution. 
031
032 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
033 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
034 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
035 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
036 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
037 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
038 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
039 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
040 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
041 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042 */
043
044/**
045 * Writes data to an vCard JSON data stream (jCard).
046 * 
047 * @author Michael Angstadt
048 * @author Buddy Gorven
049 * @see <a href="http://tools.ietf.org/html/rfc7095">RFC 7095</a>
050 */
051public class JCardRawWriter implements Closeable, Flushable {
052        private final Writer writer;
053        private final boolean wrapInArray;
054        private JsonGenerator generator;
055        private boolean prettyPrint = false;
056        private boolean open = false;
057        private boolean closeGenerator = true;
058        private PrettyPrinter prettyPrinter;
059
060        /**
061         * @param writer the writer to wrap
062         * @param wrapInArray true to wrap everything in an array, false not to
063         * (useful when writing more than one vCard)
064         */
065        public JCardRawWriter(Writer writer, boolean wrapInArray) {
066                this.writer = writer;
067                this.wrapInArray = wrapInArray;
068        }
069
070        /**
071         * @param generator the generator to write to
072         */
073        public JCardRawWriter(JsonGenerator generator) {
074                this.writer = null;
075                this.generator = generator;
076                this.closeGenerator = false;
077                this.wrapInArray = false;
078        }
079
080        /**
081         * Gets whether or not the JSON will be pretty-printed.
082         * @return true if it will be pretty-printed, false if not (defaults to
083         * false)
084         */
085        public boolean isPrettyPrint() {
086                return prettyPrint;
087        }
088
089        /**
090         * Sets whether or not to pretty-print the JSON.
091         * @param prettyPrint true to pretty-print it, false not to (defaults to
092         * false)
093         */
094        public void setPrettyPrint(boolean prettyPrint) {
095                this.prettyPrint = prettyPrint;
096        }
097
098        /**
099         * Sets the pretty printer to pretty-print the JSON with. Note that this
100         * method implicitly enables indenting, so {@code setPrettyPrint(true)} does
101         * not also need to be called.
102         * @param prettyPrinter the custom pretty printer (defaults to an instance
103         * of {@link JCardPrettyPrinter}, if {@code setPrettyPrint(true)} has been
104         * called)
105         */
106        public void setPrettyPrinter(PrettyPrinter prettyPrinter) {
107                prettyPrint = true;
108                this.prettyPrinter = prettyPrinter;
109        }
110
111        /**
112         * Writes the beginning of a new "vcard" component.
113         * @throws IOException if there's a problem writing to the output stream
114         */
115        public void writeStartVCard() throws IOException {
116                if (generator == null) {
117                        init();
118                }
119
120                if (open) {
121                        writeEndVCard();
122                }
123
124                generator.writeStartArray();
125                generator.writeString("vcard");
126                generator.writeStartArray(); //start properties array
127
128                open = true;
129        }
130
131        /**
132         * Closes the "vcard" component array.
133         * @throws IllegalStateException if the component was never opened (
134         * {@link #writeStartVCard} must be called first)
135         * @throws IOException if there's a problem writing to the output stream
136         */
137        public void writeEndVCard() throws IOException {
138                if (!open) {
139                        throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(1));
140                }
141
142                generator.writeEndArray(); //end the properties array
143                generator.writeEndArray(); //end the "vcard" component array
144
145                open = false;
146        }
147
148        /**
149         * Writes a property to the current component.
150         * @param propertyName the property name (e.g. "version")
151         * @param dataType the data type or null for "unknown"
152         * @param value the property value
153         * @throws IllegalStateException if the "vcard" component was never opened
154         * or was just closed ({@link #writeStartVCard} must be called first)
155         * @throws IOException if there's a problem writing to the output stream
156         */
157        public void writeProperty(String propertyName, VCardDataType dataType, JCardValue value) throws IOException {
158                writeProperty(null, propertyName, new VCardParameters(), dataType, value);
159        }
160
161        /**
162         * Writes a property to the current vCard.
163         * @param group the group or null if there is no group
164         * @param propertyName the property name (e.g. "version")
165         * @param parameters the parameters
166         * @param dataType the data type or null for "unknown"
167         * @param value the property value
168         * @throws IllegalStateException if the "vcard" component was never opened
169         * or was just closed ({@link #writeStartVCard} must be called first)
170         * @throws IOException if there's a problem writing to the output stream
171         */
172        public void writeProperty(String group, String propertyName, VCardParameters parameters, VCardDataType dataType, JCardValue value) throws IOException {
173                if (!open) {
174                        throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(1));
175                }
176
177                generator.assignCurrentValue(JCardPrettyPrinter.PROPERTY_VALUE);
178
179                generator.writeStartArray();
180
181                //write the property name
182                generator.writeString(propertyName);
183
184                //write parameters and group
185                writeParametersAndGroup(parameters, group);
186
187                //write data type
188                generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
189
190                //write value
191                if (value.getValues().isEmpty()) {
192                        generator.writeString("");
193                } else {
194                        for (JsonValue jsonValue : value.getValues()) {
195                                writeJsonValue(jsonValue);
196                        }
197                }
198
199                generator.writeEndArray();
200
201                generator.assignCurrentValue(null);
202        }
203
204        private void writeParametersAndGroup(VCardParameters parameters, String group) throws IOException {
205                generator.writeStartObject();
206
207                for (Map.Entry<String, List<String>> entry : parameters) {
208                        String name = entry.getKey().toLowerCase();
209                        List<String> values = entry.getValue();
210                        if (values.isEmpty()) {
211                                continue;
212                        }
213
214                        if (values.size() == 1) {
215                                generator.writeStringField(name, values.get(0));
216                        } else {
217                                generator.writeArrayFieldStart(name);
218                                for (String value : values) {
219                                        generator.writeString(value);
220                                }
221                                generator.writeEndArray();
222                        }
223                }
224
225                //write group
226                if (group != null) {
227                        generator.writeStringField("group", group);
228                }
229
230                generator.writeEndObject();
231        }
232
233        private void writeJsonValue(JsonValue jsonValue) throws IOException {
234                if (jsonValue.isNull()) {
235                        generator.writeNull();
236                        return;
237                }
238
239                Object val = jsonValue.getValue();
240                if (val != null) {
241                        writeValue(val);
242                        return;
243                }
244
245                List<JsonValue> array = jsonValue.getArray();
246                if (array != null) {
247                        writeArray(array);
248                        return;
249                }
250
251                Map<String, JsonValue> object = jsonValue.getObject();
252                if (object != null) {
253                        writeObject(object);
254                        return;
255                }
256        }
257
258        private void writeValue(Object val) throws IOException {
259                if (val instanceof Byte) {
260                        generator.writeNumber((Byte) val);
261                } else if (val instanceof Short) {
262                        generator.writeNumber((Short) val);
263                } else if (val instanceof Integer) {
264                        generator.writeNumber((Integer) val);
265                } else if (val instanceof Long) {
266                        generator.writeNumber((Long) val);
267                } else if (val instanceof Float) {
268                        generator.writeNumber((Float) val);
269                } else if (val instanceof Double) {
270                        generator.writeNumber((Double) val);
271                } else if (val instanceof Boolean) {
272                        generator.writeBoolean((Boolean) val);
273                } else {
274                        generator.writeString(val.toString());
275                }
276        }
277
278        private void writeArray(List<JsonValue> array) throws IOException {
279                generator.writeStartArray();
280                for (JsonValue element : array) {
281                        writeJsonValue(element);
282                }
283                generator.writeEndArray();
284        }
285
286        private void writeObject(Map<String, JsonValue> object) throws IOException {
287                generator.writeStartObject();
288                for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
289                        generator.writeFieldName(entry.getKey());
290                        writeJsonValue(entry.getValue());
291                }
292                generator.writeEndObject();
293        }
294
295        /**
296         * Flushes the JSON stream.
297         * @throws IOException if there's a problem writing to the output stream
298         */
299        public void flush() throws IOException {
300                if (generator == null) {
301                        return;
302                }
303
304                generator.flush();
305        }
306
307        /**
308         * Finishes writing the JSON document so that it is syntactically correct.
309         * No more data can be written once this method is called.
310         * @throws IOException if there's a problem closing the output stream
311         */
312        public void closeJsonStream() throws IOException {
313                if (generator == null) {
314                        return;
315                }
316
317                while (open) {
318                        writeEndVCard();
319                }
320
321                if (wrapInArray) {
322                        generator.writeEndArray();
323                }
324
325                if (closeGenerator) {
326                        generator.close();
327                }
328        }
329
330        /**
331         * Finishes writing the JSON document and closes the underlying
332         * {@link Writer}.
333         * @throws IOException if there's a problem closing the output stream
334         */
335        public void close() throws IOException {
336                if (generator == null) {
337                        return;
338                }
339
340                closeJsonStream();
341                if (writer != null) {
342                        writer.close();
343                }
344        }
345
346        private void init() throws IOException {
347                JsonFactory factory = new JsonFactory();
348                factory.configure(Feature.AUTO_CLOSE_TARGET, false);
349                generator = factory.createGenerator(writer);
350
351                if (prettyPrint) {
352                        if (prettyPrinter == null) {
353                                prettyPrinter = new JCardPrettyPrinter();
354                        }
355                        generator.setPrettyPrinter(prettyPrinter);
356                }
357
358                if (wrapInArray) {
359                        generator.writeStartArray();
360                }
361        }
362}