001package ezvcard.io.json;
002
003import java.io.Flushable;
004import java.io.IOException;
005import java.io.OutputStream;
006import java.io.OutputStreamWriter;
007import java.io.Writer;
008import java.nio.charset.StandardCharsets;
009import java.nio.file.Files;
010import java.nio.file.Path;
011import java.util.List;
012
013import com.fasterxml.jackson.core.JsonGenerator;
014import com.fasterxml.jackson.core.PrettyPrinter;
015
016import ezvcard.VCard;
017import ezvcard.VCardDataType;
018import ezvcard.VCardVersion;
019import ezvcard.io.EmbeddedVCardException;
020import ezvcard.io.SkipMeException;
021import ezvcard.io.StreamWriter;
022import ezvcard.io.scribe.VCardPropertyScribe;
023import ezvcard.parameter.VCardParameters;
024import ezvcard.property.VCardProperty;
025
026/*
027 Copyright (c) 2012-2023, Michael Angstadt
028 All rights reserved.
029
030 Redistribution and use in source and binary forms, with or without
031 modification, are permitted provided that the following conditions are met: 
032
033 1. Redistributions of source code must retain the above copyright notice, this
034 list of conditions and the following disclaimer. 
035 2. Redistributions in binary form must reproduce the above copyright notice,
036 this list of conditions and the following disclaimer in the documentation
037 and/or other materials provided with the distribution. 
038
039 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
040 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
041 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
042 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
043 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
044 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
045 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
046 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
047 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
048 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
049
050 The views and conclusions contained in the software and documentation are those
051 of the authors and should not be interpreted as representing official policies, 
052 either expressed or implied, of the FreeBSD Project.
053 */
054
055/**
056 * <p>
057 * Writes {@link VCard} objects to a JSON data stream (jCard format).
058 * </p>
059 * <p>
060 * <b>Example:</b>
061 * </p>
062 * 
063 * <pre class="brush:java">
064 * VCard vcard1 = ...
065 * VCard vcard2 = ...
066 * Path file = Paths.get("vcard.json");
067 * try (JCardWriter writer = new JCardWriter(file)) {
068 *   writer.write(vcard1);
069 *   writer.write(vcard2);
070 * }
071 * </pre>
072 * @author Michael Angstadt
073 * @author Buddy Gorven
074 * @see <a href="http://tools.ietf.org/html/rfc7095">RFC 7095</a>
075 */
076public class JCardWriter extends StreamWriter implements Flushable {
077        private final JCardRawWriter writer;
078        private final VCardVersion targetVersion = VCardVersion.V4_0;
079        private JsonGenerator generator = null;
080
081        /**
082         * @param out the output stream to write to (UTF-8 encoding will be used)
083         */
084        public JCardWriter(OutputStream out) {
085                this(new OutputStreamWriter(out, StandardCharsets.UTF_8));
086        }
087
088        /**
089         * @param out the output stream to write to (UTF-8 encoding will be used)
090         * @param wrapInArray true to enclose all written vCards in a JSON array,
091         * false not to
092         */
093        public JCardWriter(OutputStream out, boolean wrapInArray) {
094                this(new OutputStreamWriter(out, StandardCharsets.UTF_8), wrapInArray);
095        }
096
097        /**
098         * @param file the file to write to (UTF-8 encoding will be used)
099         * @throws IOException if there's a problem opening the file
100         */
101        public JCardWriter(Path file) throws IOException {
102                this(Files.newBufferedWriter(file, StandardCharsets.UTF_8));
103        }
104
105        /**
106         * @param file the file to write to (UTF-8 encoding will be used)
107         * @param wrapInArray true to enclose all written vCards in a JSON array,
108         * false not to
109         * @throws IOException if there's a problem opening the file
110         */
111        public JCardWriter(Path file, boolean wrapInArray) throws IOException {
112                this(Files.newBufferedWriter(file, StandardCharsets.UTF_8), wrapInArray);
113        }
114
115        /**
116         * @param writer the writer to write to
117         */
118        public JCardWriter(Writer writer) {
119                this(writer, false);
120        }
121
122        /**
123         * @param writer the writer to write to
124         * @param wrapInArray true to enclose all written vCards in a JSON array,
125         * false not to
126         */
127        public JCardWriter(Writer writer, boolean wrapInArray) {
128                this.writer = new JCardRawWriter(writer, wrapInArray);
129        }
130
131        /**
132         * @param generator the generator to write to
133         */
134        public JCardWriter(JsonGenerator generator) {
135                this.generator = generator;
136                this.writer = new JCardRawWriter(generator);
137        }
138
139        /**
140         * Writes a vCard to the stream.
141         * @param vcard the vCard that is being written
142         * @param properties the properties to write
143         * @throws IOException if there's a problem writing to the output stream
144         * @throws IllegalArgumentException if a scribe hasn't been registered for a
145         * custom property class (see: {@link #registerScribe registerScribe})
146         */
147        @Override
148        @SuppressWarnings({ "rawtypes", "unchecked" })
149        protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException {
150                Object previousValue = getCurrentValue();
151
152                writer.writeStartVCard();
153                writer.writeProperty("version", VCardDataType.TEXT, JCardValue.single(targetVersion.getVersion()));
154
155                for (VCardProperty property : properties) {
156                        VCardPropertyScribe scribe = index.getPropertyScribe(property);
157
158                        //marshal the value
159                        JCardValue value;
160                        try {
161                                value = scribe.writeJson(property);
162                        } catch (SkipMeException e) {
163                                //property has requested not to be written
164                                continue;
165                        } catch (EmbeddedVCardException e) {
166                                //don't write because jCard does not support embedded vCards
167                                continue;
168                        }
169
170                        String group = property.getGroup();
171                        String name = scribe.getPropertyName().toLowerCase();
172                        VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard);
173                        removeUnsupportedParameters(parameters);
174                        VCardDataType dataType = scribe.dataType(property, targetVersion);
175
176                        writer.writeProperty(group, name, parameters, dataType, value);
177                }
178
179                writer.writeEndVCard();
180
181                setCurrentValue(previousValue);
182        }
183
184        /**
185         * If this object has a {@link JsonGenerator), and the generator has an
186         * output context, gets the current value of the output context.
187         * 
188         * @return the value of the object that is currently being serialized, if
189         *         available
190         */
191        private Object getCurrentValue() {
192                return (generator == null) ? null : generator.getCurrentValue();
193        }
194
195        /**
196         * If this object has a {@link JsonGenerator), and the generator has an
197         * output context, sets the current value of the output context.
198         * 
199         * @param value
200         *            the object that is currently being serialized
201         */
202        private void setCurrentValue(Object value) {
203                if (generator != null) {
204                        generator.setCurrentValue(value);
205                }
206        }
207
208        @Override
209        protected VCardVersion getTargetVersion() {
210                return targetVersion;
211        }
212
213        /**
214         * Gets whether or not the JSON will be pretty-printed.
215         * @return true if it will be pretty-printed, false if not (defaults to
216         * false)
217         */
218        public boolean isPrettyPrint() {
219                return writer.isPrettyPrint();
220        }
221
222        /**
223         * Sets whether or not to pretty-print the JSON.
224         * @param prettyPrint true to pretty-print it, false not to (defaults to
225         * false)
226         */
227        public void setPrettyPrint(boolean prettyPrint) {
228                writer.setPrettyPrint(prettyPrint);
229        }
230
231        /**
232         * Sets the pretty printer to pretty-print the JSON with. Note that this
233         * method implicitly enables indenting, so {@code setPrettyPrint(true)} does
234         * not also need to be called.
235         * @param prettyPrinter the custom pretty printer (defaults to an instance
236         * of {@link JCardPrettyPrinter}, if {@code setPrettyPrint(true)} has been
237         * called.
238         */
239        public void setPrettyPrinter(PrettyPrinter prettyPrinter) {
240                writer.setPrettyPrinter(prettyPrinter);
241        }
242
243        /**
244         * Removes parameters that are not supported by jCard.
245         * @param parameters the property parameters
246         */
247        private void removeUnsupportedParameters(VCardParameters parameters) {
248                parameters.setCharset(null);
249                parameters.setEncoding(null);
250                parameters.setValue(null);
251        }
252
253        /**
254         * Flushes the jCard data stream.
255         * @throws IOException if there's a problem flushing the stream
256         */
257        public void flush() throws IOException {
258                writer.flush();
259        }
260
261        /**
262         * Ends the jCard data stream, but does not close the underlying writer.
263         * @throws IOException if there's a problem closing the stream
264         */
265        public void closeJsonStream() throws IOException {
266                writer.closeJsonStream();
267        }
268
269        /**
270         * Ends the jCard data stream and closes the underlying writer.
271         * @throws IOException if there's a problem closing the stream
272         */
273        public void close() throws IOException {
274                writer.close();
275        }
276}