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