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}