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-2023, 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.setCurrentValue(JCardPrettyPrinter.PROPERTY_VALUE);
178
179                generator.writeStartArray();
180
181                //write the property name
182                generator.writeString(propertyName);
183
184                //write parameters
185                generator.writeStartObject();
186                for (Map.Entry<String, List<String>> entry : parameters) {
187                        String name = entry.getKey().toLowerCase();
188                        List<String> values = entry.getValue();
189                        if (values.isEmpty()) {
190                                continue;
191                        }
192
193                        if (values.size() == 1) {
194                                generator.writeStringField(name, values.get(0));
195                        } else {
196                                generator.writeArrayFieldStart(name);
197                                for (String paramValue : values) {
198                                        generator.writeString(paramValue);
199                                }
200                                generator.writeEndArray();
201                        }
202                }
203
204                //write group
205                if (group != null) {
206                        generator.writeStringField("group", group);
207                }
208
209                //end parameters object
210                generator.writeEndObject();
211
212                //write data type
213                generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
214
215                //write value
216                if (value.getValues().isEmpty()) {
217                        generator.writeString("");
218                } else {
219                        for (JsonValue jsonValue : value.getValues()) {
220                                writeValue(jsonValue);
221                        }
222                }
223
224                generator.writeEndArray();
225
226                generator.setCurrentValue(null);
227        }
228
229        private void writeValue(JsonValue jsonValue) throws IOException {
230                if (jsonValue.isNull()) {
231                        generator.writeNull();
232                        return;
233                }
234
235                Object val = jsonValue.getValue();
236                if (val != null) {
237                        if (val instanceof Byte) {
238                                generator.writeNumber((Byte) val);
239                        } else if (val instanceof Short) {
240                                generator.writeNumber((Short) val);
241                        } else if (val instanceof Integer) {
242                                generator.writeNumber((Integer) val);
243                        } else if (val instanceof Long) {
244                                generator.writeNumber((Long) val);
245                        } else if (val instanceof Float) {
246                                generator.writeNumber((Float) val);
247                        } else if (val instanceof Double) {
248                                generator.writeNumber((Double) val);
249                        } else if (val instanceof Boolean) {
250                                generator.writeBoolean((Boolean) val);
251                        } else {
252                                generator.writeString(val.toString());
253                        }
254                        return;
255                }
256
257                List<JsonValue> array = jsonValue.getArray();
258                if (array != null) {
259                        generator.writeStartArray();
260                        for (JsonValue element : array) {
261                                writeValue(element);
262                        }
263                        generator.writeEndArray();
264                        return;
265                }
266
267                Map<String, JsonValue> object = jsonValue.getObject();
268                if (object != null) {
269                        generator.writeStartObject();
270                        for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
271                                generator.writeFieldName(entry.getKey());
272                                writeValue(entry.getValue());
273                        }
274                        generator.writeEndObject();
275                        return;
276                }
277        }
278
279        /**
280         * Flushes the JSON stream.
281         * @throws IOException if there's a problem writing to the output stream
282         */
283        public void flush() throws IOException {
284                if (generator == null) {
285                        return;
286                }
287
288                generator.flush();
289        }
290
291        /**
292         * Finishes writing the JSON document so that it is syntactically correct.
293         * No more data can be written once this method is called.
294         * @throws IOException if there's a problem closing the output stream
295         */
296        public void closeJsonStream() throws IOException {
297                if (generator == null) {
298                        return;
299                }
300
301                while (open) {
302                        writeEndVCard();
303                }
304
305                if (wrapInArray) {
306                        generator.writeEndArray();
307                }
308
309                if (closeGenerator) {
310                        generator.close();
311                }
312        }
313
314        /**
315         * Finishes writing the JSON document and closes the underlying
316         * {@link Writer}.
317         * @throws IOException if there's a problem closing the output stream
318         */
319        public void close() throws IOException {
320                if (generator == null) {
321                        return;
322                }
323
324                closeJsonStream();
325                if (writer != null) {
326                        writer.close();
327                }
328        }
329
330        private void init() throws IOException {
331                JsonFactory factory = new JsonFactory();
332                factory.configure(Feature.AUTO_CLOSE_TARGET, false);
333                generator = factory.createGenerator(writer);
334
335                if (prettyPrint) {
336                        if (prettyPrinter == null) {
337                                prettyPrinter = new JCardPrettyPrinter();
338                        }
339                        generator.setPrettyPrinter(prettyPrinter);
340                }
341
342                if (wrapInArray) {
343                        generator.writeStartArray();
344                }
345        }
346}