001package ezvcard.io;
002
003import java.io.Closeable;
004import java.io.IOException;
005import java.util.HashSet;
006import java.util.LinkedList;
007import java.util.List;
008import java.util.Set;
009import java.util.stream.Collectors;
010
011import ezvcard.Ezvcard;
012import ezvcard.Messages;
013import ezvcard.VCard;
014import ezvcard.VCardVersion;
015import ezvcard.io.scribe.ScribeIndex;
016import ezvcard.io.scribe.VCardPropertyScribe;
017import ezvcard.property.Address;
018import ezvcard.property.Label;
019import ezvcard.property.ProductId;
020import ezvcard.property.RawProperty;
021import ezvcard.property.VCardProperty;
022
023/*
024 Copyright (c) 2012-2026, Michael Angstadt
025 All rights reserved.
026
027 Redistribution and use in source and binary forms, with or without
028 modification, are permitted provided that the following conditions are met: 
029
030 1. Redistributions of source code must retain the above copyright notice, this
031 list of conditions and the following disclaimer. 
032 2. Redistributions in binary form must reproduce the above copyright notice,
033 this list of conditions and the following disclaimer in the documentation
034 and/or other materials provided with the distribution. 
035
036 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
037 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
038 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
039 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
040 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
041 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
042 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
043 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
044 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
045 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
046
047 The views and conclusions contained in the software and documentation are those
048 of the authors and should not be interpreted as representing official policies, 
049 either expressed or implied, of the FreeBSD Project.
050 */
051
052/**
053 * Writes vCards to a data stream.
054 * @author Michael Angstadt
055 */
056public abstract class StreamWriter implements Closeable {
057        protected ScribeIndex index = new ScribeIndex();
058        protected boolean addProdId = true;
059        protected boolean versionStrict = true;
060
061        /**
062         * Writes a vCard to the stream.
063         * @param vcard the vCard that is being written
064         * @throws IOException if there's a problem writing to the output stream
065         * @throws IllegalArgumentException if a scribe hasn't been registered for a
066         * custom property class (see: {@link #registerScribe registerScribe})
067         */
068        public void write(VCard vcard) throws IOException {
069                List<VCardProperty> properties = prepare(vcard);
070                _write(vcard, properties);
071        }
072
073        /**
074         * Writes a vCard to the stream.
075         * @param vcard the vCard that is being written
076         * @param properties the properties to write
077         * @throws IOException if there's a problem writing to the output stream
078         */
079        protected abstract void _write(VCard vcard, List<VCardProperty> properties) throws IOException;
080
081        /**
082         * Gets the version that the next vCard will be written as.
083         * @return the version
084         */
085        protected abstract VCardVersion getTargetVersion();
086
087        /**
088         * Gets whether a {@link ProductId} property will be added to each vCard
089         * that marks it as having been generated by this library. For 2.1 vCards,
090         * the extended property "X-PRODID" will be added, since {@link ProductId}
091         * is not supported by that version.
092         * @return true if the property will be added, false if not (defaults to
093         * true)
094         */
095        public boolean isAddProdId() {
096                return addProdId;
097        }
098
099        /**
100         * Sets whether to add a {@link ProductId} property to each vCard that marks
101         * it as having been generated by this library. For 2.1 vCards, the extended
102         * property "X-PRODID" will be added, since {@link ProductId} is not
103         * supported by that version.
104         * @param addProdId true to add the property, false not to (defaults to
105         * true)
106         */
107        public void setAddProdId(boolean addProdId) {
108                this.addProdId = addProdId;
109        }
110
111        /**
112         * Gets whether properties that do not support the target version will be
113         * excluded from the written vCard.
114         * @return true to exclude properties that do not support the target
115         * version, false not to (defaults to true)
116         */
117        public boolean isVersionStrict() {
118                return versionStrict;
119        }
120
121        /**
122         * Sets whether to exclude properties that do not support the target version
123         * from the written vCard.
124         * @param versionStrict true to exclude such properties, false not to
125         * (defaults to true)
126         */
127        public void setVersionStrict(boolean versionStrict) {
128                this.versionStrict = versionStrict;
129        }
130
131        /**
132         * <p>
133         * Registers a property scribe. This is the same as calling:
134         * </p>
135         * <p>
136         * {@code getScribeIndex().register(scribe)}
137         * </p>
138         * @param scribe the scribe to register
139         */
140        public void registerScribe(VCardPropertyScribe<? extends VCardProperty> scribe) {
141                index.register(scribe);
142        }
143
144        /**
145         * Gets the scribe index.
146         * @return the scribe index
147         */
148        public ScribeIndex getScribeIndex() {
149                return index;
150        }
151
152        /**
153         * Sets the scribe index.
154         * @param index the scribe index
155         */
156        public void setScribeIndex(ScribeIndex index) {
157                this.index = index;
158        }
159
160        /**
161         * Determines which properties need to be written.
162         * @param vcard the vCard to write
163         * @return the properties to write
164         * @throws IllegalArgumentException if a scribe hasn't been registered for a
165         * custom property class (see: {@link #registerScribe(VCardPropertyScribe)
166         * registerScribe})
167         */
168        private List<VCardProperty> prepare(VCard vcard) {
169                VCardVersion targetVersion = getTargetVersion();
170                List<VCardProperty> propertiesToAdd = new LinkedList<>();
171                Set<Class<? extends VCardProperty>> unregistered = new HashSet<>();
172                VCardProperty prodIdProperty = null;
173                for (VCardProperty property : vcard) {
174                        if (versionStrict && !property.isSupportedBy(targetVersion)) {
175                                //do not add the property to the vCard if it is not supported by the target version
176                                continue;
177                        }
178
179                        //do not add PRODID to the property list yet
180                        if (property instanceof ProductId) {
181                                prodIdProperty = property;
182                                continue;
183                        }
184
185                        //check for scribe
186                        if (!index.hasPropertyScribe(property)) {
187                                unregistered.add(property.getClass());
188                                continue;
189                        }
190
191                        propertiesToAdd.add(property);
192
193                        //add LABEL properties for each ADR property if the target version is 2.1 or 3.0
194                        if ((targetVersion == VCardVersion.V2_1 || targetVersion == VCardVersion.V3_0) && property instanceof Address) {
195                                createLabel((Address) property, propertiesToAdd);
196                        }
197                }
198
199                if (!unregistered.isEmpty()) {
200                        throw createUnregisteredScribesException(unregistered);
201                }
202
203                //create a PRODID property, saying the vCard was generated by this library
204                if (addProdId) {
205                        prodIdProperty = createProdId(targetVersion);
206                }
207
208                //add PRODID to the beginning of the vCard
209                if (prodIdProperty != null) {
210                        propertiesToAdd.add(0, prodIdProperty);
211                }
212
213                return propertiesToAdd;
214        }
215
216        private void createLabel(Address property, List<VCardProperty> propertiesToAdd) {
217                String labelStr = property.getLabel();
218                if (labelStr == null) {
219                        return;
220                }
221
222                Label label = new Label(labelStr);
223                label.getTypes().addAll(property.getTypes());
224                propertiesToAdd.add(label);
225        }
226
227        private IllegalArgumentException createUnregisteredScribesException(Set<Class<? extends VCardProperty>> unregistered) {
228                //@formatter:off
229                List<String> classes = unregistered.stream()
230                        .map(Class::getName)
231                .collect(Collectors.toList());
232                //@formatter:on
233
234                return Messages.INSTANCE.getIllegalArgumentException(14, classes);
235        }
236
237        private VCardProperty createProdId(VCardVersion targetVersion) {
238                if (targetVersion == VCardVersion.V2_1) {
239                        return new RawProperty("X-PRODID", "ez-vcard " + Ezvcard.VERSION);
240                }
241                return new ProductId("ez-vcard " + Ezvcard.VERSION);
242        }
243}