001package ezvcard.property;
002
003import java.lang.reflect.Constructor;
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collections;
007import java.util.List;
008import java.util.Map;
009import java.util.Objects;
010
011import com.github.mangstadt.vinnie.SyntaxStyle;
012import com.github.mangstadt.vinnie.validate.AllowedCharacters;
013import com.github.mangstadt.vinnie.validate.VObjectValidator;
014
015import ezvcard.Messages;
016import ezvcard.SupportedVersions;
017import ezvcard.VCard;
018import ezvcard.VCardVersion;
019import ezvcard.ValidationWarning;
020import ezvcard.parameter.Pid;
021import ezvcard.parameter.VCardParameters;
022import ezvcard.util.StringUtils;
023
024/*
025 Copyright (c) 2012-2026, Michael Angstadt
026 All rights reserved.
027
028 Redistribution and use in source and binary forms, with or without
029 modification, are permitted provided that the following conditions are met: 
030
031 1. Redistributions of source code must retain the above copyright notice, this
032 list of conditions and the following disclaimer. 
033 2. Redistributions in binary form must reproduce the above copyright notice,
034 this list of conditions and the following disclaimer in the documentation
035 and/or other materials provided with the distribution. 
036
037 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
038 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
039 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
040 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
041 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
042 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
043 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
044 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
045 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
046 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
047
048 The views and conclusions contained in the software and documentation are those
049 of the authors and should not be interpreted as representing official policies, 
050 either expressed or implied, of the FreeBSD Project.
051 */
052
053/**
054 * Base class for all vCard property classes.
055 * @author Michael Angstadt
056 */
057public abstract class VCardProperty implements Comparable<VCardProperty> {
058        /**
059         * The group that this property belongs to or null if it doesn't belong to a
060         * group.
061         */
062        protected String group;
063
064        /**
065         * The property's parameters.
066         */
067        protected VCardParameters parameters;
068
069        protected VCardProperty() {
070                parameters = new VCardParameters();
071        }
072
073        /**
074         * Copy constructor.
075         * @param original the property to make a copy of
076         */
077        protected VCardProperty(VCardProperty original) {
078                group = original.group;
079                parameters = new VCardParameters(original.parameters);
080        }
081
082        /**
083         * <p>
084         * Gets the vCard versions that support this property.
085         * </p>
086         * <p>
087         * The supported versions are defined by assigning a
088         * {@link SupportedVersions @SupportedVersions} annotation to the property
089         * class. Property classes without this annotation are considered to be
090         * supported by all versions.
091         * </p>
092         * @return the vCard versions that support this property.
093         */
094        public final VCardVersion[] getSupportedVersions() {
095                SupportedVersions supportedVersionsAnnotation = getClass().getAnnotation(SupportedVersions.class);
096                return (supportedVersionsAnnotation == null) ? VCardVersion.values() : supportedVersionsAnnotation.value();
097        }
098
099        /**
100         * <p>
101         * Determines if this property is supported by the given vCard version.
102         * </p>
103         * <p>
104         * The supported versions are defined by assigning a
105         * {@link SupportedVersions} annotation to the property class. Property
106         * classes without this annotation are considered to be supported by all
107         * versions.
108         * </p>
109         * @param version the vCard version
110         * @return true if it is supported, false if not
111         */
112        public final boolean isSupportedBy(VCardVersion version) {
113                return Arrays.stream(getSupportedVersions()).anyMatch(supportedVersion -> supportedVersion == version);
114        }
115
116        /**
117         * Checks the property for data consistency problems or deviations from the
118         * spec. These problems will not prevent the property from being written to
119         * a data stream, but may prevent it from being parsed correctly by the
120         * consuming application. These problems can largely be avoided by reading
121         * the Javadocs of the property class, or by being familiar with the vCard
122         * standard.
123         * @param version the version to check the property against (use 4.0 for
124         * xCard and jCard)
125         * @param vcard the vCard this property belongs to
126         * @see VCard#validate
127         * @return a list of warnings or an empty list if no problems were found
128         */
129        public final List<ValidationWarning> validate(VCardVersion version, VCard vcard) {
130                List<ValidationWarning> warnings = new ArrayList<>(0);
131
132                //check the supported versions
133                if (!isSupportedBy(version)) {
134                        warnings.add(new ValidationWarning(2, Arrays.toString(getSupportedVersions())));
135                }
136
137                //check parameters
138                warnings.addAll(parameters.validate(version));
139
140                //check group
141                if (group != null) {
142                        SyntaxStyle syntax = version.getSyntaxStyle();
143                        AllowedCharacters allowed = VObjectValidator.allowedCharactersGroup(syntax, true);
144                        if (!allowed.check(group)) {
145                                if (syntax == SyntaxStyle.OLD) {
146                                        AllowedCharacters notAllowed = allowed.flip();
147                                        warnings.add(new ValidationWarning(32, group, notAllowed.toString(true)));
148                                } else {
149                                        warnings.add(new ValidationWarning(23, group));
150                                }
151                        }
152                }
153
154                _validate(warnings, version, vcard);
155
156                return warnings;
157        }
158
159        /**
160         * Checks the property for data consistency problems or deviations from the
161         * spec. Meant to be overridden by child classes that wish to provide
162         * validation logic.
163         * @param warnings the list to add the warnings to
164         * @param version the version to check the property against
165         * @param vcard the vCard this property belongs to
166         */
167        protected void _validate(List<ValidationWarning> warnings, VCardVersion version, VCard vcard) {
168                //empty
169        }
170
171        /**
172         * Gets all of the property's parameters.
173         * @return the property's parameters
174         */
175        public VCardParameters getParameters() {
176                return parameters;
177        }
178
179        /**
180         * Sets the property's parameters.
181         * @param parameters the parameters (cannot be null)
182         */
183        public void setParameters(VCardParameters parameters) {
184                if (parameters == null) {
185                        throw new NullPointerException(Messages.INSTANCE.getExceptionMessage(42));
186                }
187                this.parameters = parameters;
188        }
189
190        /**
191         * Gets the first value of a parameter.
192         * @param name the parameter name (case insensitive, e.g. "LANGUAGE")
193         * @return the parameter value or null if not found
194         */
195        public String getParameter(String name) {
196                return parameters.first(name);
197        }
198
199        /**
200         * Gets all values of a parameter.
201         * @param name the parameter name (case insensitive, e.g. "LANGUAGE")
202         * @return the parameter values (this list is immutable)
203         */
204        public List<String> getParameters(String name) {
205                return Collections.unmodifiableList(parameters.get(name));
206        }
207
208        /**
209         * Replaces all existing values of a parameter with the given value.
210         * @param name the parameter name (case insensitive, e.g. "LANGUAGE")
211         * @param value the parameter value
212         */
213        public void setParameter(String name, String value) {
214                parameters.replace(name, value);
215        }
216
217        /**
218         * Adds a value to a parameter.
219         * @param name the parameter name (case insensitive, e.g. "LANGUAGE")
220         * @param value the parameter value
221         */
222        public void addParameter(String name, String value) {
223                parameters.put(name, value);
224        }
225
226        /**
227         * Removes a parameter from the property.
228         * @param name the parameter name (case insensitive, e.g. "LANGUAGE")
229         */
230        public void removeParameter(String name) {
231                parameters.removeAll(name);
232        }
233
234        /**
235         * Gets this property's group.
236         * @return the group or null if it does not belong to a group
237         */
238        public String getGroup() {
239                return group;
240        }
241
242        /**
243         * Sets this property's group.
244         * @param group the group or null to remove the property's group
245         */
246        public void setGroup(String group) {
247                this.group = group;
248        }
249
250        /**
251         * Sorts by PREF parameter ascending. Properties that do not have a PREF
252         * parameter are pushed to the end of the list.
253         */
254        public int compareTo(VCardProperty that) {
255                Integer pref0 = this.getParameters().getPref();
256                Integer pref1 = that.getParameters().getPref();
257                if (pref0 == null && pref1 == null) {
258                        return 0;
259                }
260                if (pref0 == null) {
261                        return 1;
262                }
263                if (pref1 == null) {
264                        return -1;
265                }
266                return pref1.compareTo(pref0);
267        }
268
269        /**
270         * <p>
271         * Gets string representations of the class's fields for the
272         * {@link #toString} method.
273         * </p>
274         * <p>
275         * Meant to be overridden by child classes. The default implementation
276         * returns an empty map.
277         * </p>
278         * @return the values of the class's fields (key = field name, value = field
279         * value)
280         */
281        protected Map<String, Object> toStringValues() {
282                return Collections.emptyMap();
283        }
284
285        @Override
286        public String toString() {
287                StringBuilder sb = new StringBuilder();
288                sb.append(getClass().getName());
289                sb.append(" [ group=").append(group);
290                sb.append(" | parameters=").append(parameters);
291                toStringValues().forEach((fieldName, fieldValue) -> sb.append(" | ").append(fieldName).append('=').append(fieldValue));
292                sb.append(" ]");
293                return sb.toString();
294        }
295
296        /**
297         * <p>
298         * Creates a copy of this property object.
299         * </p>
300         * <p>
301         * The default implementation of this method uses reflection to look for a
302         * copy constructor. Child classes SHOULD override this method to avoid the
303         * performance overhead involved in using reflection.
304         * </p>
305         * <p>
306         * The child class's copy constructor, if present, MUST invoke the
307         * {@link #VCardProperty(VCardProperty)} super constructor to ensure that
308         * the group name and parameters are also copied.
309         * </p>
310         * <p>
311         * This method MUST be overridden by the child class if the child class does
312         * not have a copy constructor. Otherwise, an
313         * {@link UnsupportedOperationException} will be thrown when an attempt is
314         * made to copy the property (such as in the {@link VCard#VCard(VCard) VCard
315         * class's copy constructor}).
316         * </p>
317         * @return the copy
318         * @throws UnsupportedOperationException if the class does not have a copy
319         * constructor or there is a problem invoking it
320         */
321        public VCardProperty copy() {
322                Class<? extends VCardProperty> clazz = getClass();
323
324                try {
325                        Constructor<? extends VCardProperty> copyConstructor = clazz.getConstructor(clazz);
326                        return copyConstructor.newInstance(this);
327                } catch (Exception e) {
328                        throw new UnsupportedOperationException(Messages.INSTANCE.getExceptionMessage(31, clazz.getName()), e);
329                }
330        }
331
332        @Override
333        public int hashCode() {
334                return StringUtils.hashIgnoreCase(group, parameters);
335        }
336
337        @Override
338        public boolean equals(Object obj) {
339                if (this == obj) return true;
340                if (obj == null) return false;
341                if (getClass() != obj.getClass()) return false;
342                VCardProperty other = (VCardProperty) obj;
343                return StringUtils.equalsIgnoreCase(group, other.group) && Objects.equals(parameters, other.parameters);
344        }
345
346        /*
347         * Note: The following parameter helper methods are package-scoped so they
348         * don't clutter up the Javadocs for the VCardProperty class. They are
349         * defined here instead of in the child classes that use them, so that their
350         * Javadocs don't have to be repeated.
351         */
352
353        /**
354         * <p>
355         * Gets the list that stores this property's PID (property ID) parameter
356         * values.
357         * </p>
358         * <p>
359         * PIDs can exist on any property where multiple instances are allowed (such
360         * as {@link Email} or {@link Address}, but not {@link StructuredName}
361         * because only 1 instance of this property is allowed per vCard).
362         * </p>
363         * <p>
364         * When used in conjunction with the {@link ClientPidMap} property, it
365         * allows an individual property instance to be uniquely identifiable. This
366         * feature is made use of when two different versions of the same vCard have
367         * to be merged together (called "synchronizing").
368         * </p>
369         * <p>
370         * <b>Supported versions:</b> {@code 4.0}
371         * </p>
372         * @return the PID parameter values (this list is mutable)
373         * @throws IllegalStateException if one or more parameter values cannot be
374         * parsed as PIDs. If this happens, you may use the
375         * {@link #getParameters(String)} method to retrieve the raw values.
376         * @see <a href="http://tools.ietf.org/html/rfc6350#page-19">RFC 6350
377         * p.19</a>
378         */
379        List<Pid> getPids() {
380                return parameters.getPids();
381        }
382
383        /**
384         * <p>
385         * Gets this property's preference value. The lower this number is, the more
386         * "preferred" the property instance is compared with other properties of
387         * the same type. If a property doesn't have a preference value, then it is
388         * considered the least preferred.
389         * </p>
390         * <p>
391         * In the vCard below, the {@link Address} on the second row is the most
392         * preferred because it has the lowest PREF value.
393         * </p>
394         * 
395         * <pre>
396         * ADR;TYPE=work;PREF=2:;;1600 Amphitheatre Parkway;Mountain View;CA;94043
397         * ADR;TYPE=work;PREF=1:;;One Microsoft Way;Redmond;WA;98052
398         * ADR;TYPE=home:;;123 Maple St;Hometown;KS;12345
399         * </pre>
400         * 
401         * <p>
402         * <b>Supported versions:</b> {@code 4.0}
403         * </p>
404         * @return the preference value or null if not set
405         * @throws IllegalStateException if the parameter value cannot be parsed as
406         * an integer. If this happens, you may use the
407         * {@link #getParameter(String)} method to retrieve its raw value.
408         * @see <a href="http://tools.ietf.org/html/rfc6350#page-17">RFC 6350
409         * p.17</a>
410         */
411        Integer getPref() {
412                return parameters.getPref();
413        }
414
415        /**
416         * <p>
417         * Sets this property's preference value. The lower this number is, the more
418         * "preferred" the property instance is compared with other properties of
419         * the same type. If a property doesn't have a preference value, then it is
420         * considered the least preferred.
421         * </p>
422         * <p>
423         * In the vCard below, the {@link Address} on the second row is the most
424         * preferred because it has the lowest PREF value.
425         * </p>
426         * 
427         * <pre>
428         * ADR;TYPE=work;PREF=2:;;1600 Amphitheatre Parkway;Mountain View;CA;94043
429         * ADR;TYPE=work;PREF=1:;;One Microsoft Way;Redmond;WA;98052
430         * ADR;TYPE=home:;;123 Maple St;Hometown;KS;12345
431         * </pre>
432         * 
433         * <p>
434         * <b>Supported versions:</b> {@code 4.0}
435         * </p>
436         * @param pref the preference value or null to remove
437         * @see <a href="http://tools.ietf.org/html/rfc6350#page-17">RFC 6350
438         * p.17</a>
439         */
440        void setPref(Integer pref) {
441                parameters.setPref(pref);
442        }
443
444        /**
445         * Gets the language that the property value is written in.
446         * @return the language or null if not set
447         */
448        String getLanguage() {
449                return parameters.getLanguage();
450        }
451
452        /**
453         * Sets the language that the property value is written in.
454         * @param language the language or null to remove
455         */
456        void setLanguage(String language) {
457                parameters.setLanguage(language);
458        }
459
460        /**
461         * <p>
462         * Gets the sorted position of this property when it is grouped together
463         * with other properties of the same type. Properties with low index values
464         * are put at the beginning of the sorted list. Properties with high index
465         * values are put at the end of the list.
466         * </p>
467         * <p>
468         * <b>Supported versions:</b> {@code 4.0}
469         * </p>
470         * @return the index or null if not set
471         * @throws IllegalStateException if the parameter value cannot be parsed as
472         * an integer. If this happens, you may use the
473         * {@link #getParameter(String)} method to retrieve its raw value.
474         * @see <a href="https://tools.ietf.org/html/rfc6715#page-7">RFC 6715
475         * p.7</a>
476         */
477        Integer getIndex() {
478                return parameters.getIndex();
479        }
480
481        /**
482         * <p>
483         * Sets the sorted position of this property when it is grouped together
484         * with other properties of the same type. Properties with low index values
485         * are put at the beginning of the sorted list. Properties with high index
486         * values are put at the end of the list.
487         * </p>
488         * <p>
489         * <b>Supported versions:</b> {@code 4.0}
490         * </p>
491         * @param index the index or null to remove
492         * @throws IllegalStateException if the parameter value is malformed and
493         * cannot be parsed. If this happens, you may use the
494         * {@link #getParameter(String)} method to retrieve its raw value.
495         * @see <a href="https://tools.ietf.org/html/rfc6715#page-7">RFC 6715
496         * p.7</a>
497         */
498        void setIndex(Integer index) {
499                parameters.setIndex(index);
500        }
501}