Skip to content

Property scribe

Michael Angstadt edited this page Jul 24, 2020 · 3 revisions

ez-vcard contains a pluggable API for creating your own property classes. This can be useful if there is an extended property that your application makes frequent use of.

To make use of this API, you will need to create a property class and a property scribe class. Then, when reading or writing your vCard, you must register the property scribe with the reader/writer class's registerScribe method.

Note that this is the same framework that ez-vcard uses to implement all of the standard vCard properties. So, feel free to explore the ezvcard.property and ezvcard.io.scribe packages in the source code for more examples.

1 Property Class

The property class holds the unmarshalled value of the property. This class must extend the VCardProperty class.

The example below shows a property class for the X-MS-ANNIVERSARY property that Microsoft Outlook often adds to its vCards.

public class MSAnniversary extends VCardProperty {
  private Date date;

  public MSAnniversary(Date date){
    this.date = date;
  }

  public MSAnniversary(MSAnniversary original){
    date = (original.date == null) ? null : new Date(original.date.getTime());
  }

  public Date getDate() {
    return date;
  }

  public void setDate(Date date) {
    this.date = date;
  }

  @Override
  protected void _validate(List<Warning> warnings, VCardVersion version, VCard vcard) {
    if (date == null) {
      warnings.add(new Warning("No date is defined."));
    }

    if (date != null && date.after(new Date())) {
      warnings.add(new Warning("Anniversary date is in the future."));
    }
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((date == null) ? 0 : date.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!super.equals(obj)) return false;
    if (getClass() != obj.getClass()) return false;
    MSAnniversary other = (MSAnniversary) obj;
    if (date == null) {
      if (other.date != null) return false;
    } else if (!date.equals(other.date)) return false;
    return true;
  }

  @Override
  public MSAnniversary copy(){
    return new MSAnniversary(this);
  }
}

1.1 _validate method

Optionally, you may define a _validate method, as shown above. This method is used to verify the correctness of a property's data model. It is invoked when the VCard.validate() method is called.

Parameter Description
List<Warning> warnings The list that all validation warnings should be added to.
VCardVersion version The vCard version that the vCard is being validated under.
VCard vcard The vCard that is being validated.

1.2 copy method and copy constructor

In order to support the VCard class's copy constructor, the property class MUST have EITHER a copy method OR a copy constructor, but PREFERRABLY both.

1.3 hashCode and equals

The property class SHOULD implement the hashCode() and equals() methods in order for the VCard.equals() method to work properly. Each of these methods MUST call their super method because the parent VCardProperty class has fields of its own.

2 Scribe Class

The scribe class is responsible for reading and writing the property to a file or other data stream. Using more common terminology, you could also refer to this class as a "marshaller" or "serializer". Scribe classes must extend the VCardPropertyScribe class and implement a number of methods.

The example below shows a scribe class for our MSAnniversary property.

public class MSAnniversaryScribe extends VCardPropertyScribe<MSAnniversary> {
  public MSAnniversaryScribe() {
    super(MSAnniversary.class, "X-MS-ANNIVERSARY");
  }

  //required
  //defines the property's default data type
  @Override
  protected VCardDataType _defaultDataType(VCardVersion version) {
    return VCardDataType.DATE;
  }

  //optional
  //determines the data type based on the property value
  @Override
  protected VCardDataType _dataType(MSAnniversary property, VCardVersion version) {
    return VCardDataType.DATE;
  }

  //optional
  //tweaks the property's parameters before the property is written
  @Override
  protected void _prepareParameters(MSAnniversary property, VCardParameters copy, VCardVersion version, VCard vcard) {
    //empty
  }

  //required
  //writes the property's value to a plain-text vCard
  @Override
  protected String _writeText(MSAnniversary property, VCardVersion version) {
    return escape(write(property.getDate()));
  }

  //required
  //parses the property's value from a plain-text vCard
  @Override
  protected MSAnniversary _parseText(String value, VCardDataType dataType, VCardVersion version, VCardParameters parameters, List<String> warnings) {
    Date date = parse(unescape(value));
    return new MSAnniversary(date);
  }

  //optional
  //writes the property to an XML document (xCard)
  @Override
  protected void _writeXml(MSAnniversary property, XCardElement element) {
    Date date = property.getDate();
    if (date != null){
      element.append(VCardDataType.DATE, write(date));
    }
  }

  //optional
  //parses the property from an XML document (xCard)
  @Override
  protected MSAnniversary _parseXml(XCardElement element, VCardParameters parameters, List<String> warnings) {
    String dateStr = element.first(VCardDataType.DATE);
    if (dateStr == null) {
      throw new CannotParseException("No <date> elements found.");
    }

    Date date = parse(dateStr);
    return new MSAnniversary(date);
  }

  //optional
  //parses the property value from an HTML page (hCard)
  @Override
  protected MSAnniversary _parseHtml(HCardElement element, List<String> warnings) {
    String dateStr = element.value();
    Date date = parse(dateStr);
    return new MSAnniversary(date);
  }

  //optional
  //writes the property to a JSON stream (jCard)
  @Override
  protected JCardValue _writeJson(MSAnniversary property) {
    Date date = property.getDate();
    String dateStr = (date == null) ? "" : write(date);
    return JCardValue.single(dateStr);
  }

  //optional
  //parses the property value from a JSON stream (jCard)
  @Override
  protected MSAnniversary _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, List<String> warnings) {
    String dateStr = value.asSingle();
    Date date = parse(dateStr);
    return new MSAnniversary(date);
  }

  private String write(Date date){
    if (date == null) return "";
    DateFormat df = new SimpleDateFormat("yyyyMMdd");
    return df.format(date);
  }

  private Date parse(String dateStr){
    if (dateStr == null) return null;
    DateFormat df = new SimpleDateFormat("yyyyMMdd");
    try {
      return df.parse(dateStr);
    } catch (ParseException e){
      throw new CannotParseException("Date format is invalid.");
    }
  }
}

2.1 Constructor

The constructor calls the parent class constructor, passing in the property class and the property name.

2.2 _defaultDataType

Required. Some properties can hold different types of data. This method returns the property's default data type. If a property instance's data type is different than the default data type, then a VALUE parameter will be added to the property when it is written to a plain-text vCard. If a property instance's data type is the same as the default data type, then a VALUE parameter will not be added to the property.

2.3 _dataType

Optional. Determines a property instance's data type. If this differs from the default data type, then a VALUE parameter will be added to the property when written to a plain-text vCard.

Method argument Description
MSAnniversary property The property object that is being written.
VCardVersion version The version of the vCard that is being generated.

2.4 _prepareParameters

Optional. Allows the property's parameters to be tweaked before the property is written. A copy of the property's parameters is passed into this method so that the data in the original property object will not be modified. Our property does not use parameters, so our implementation of this method is empty.

Method argument Description
MSAnniversary property The property object that is being written.
VCardParameters copy A copy of the property's parameters. All modifications must be made to this copy.
VCardVersion version The version of the vCard that is being generated.
VCard vcard The vCard that is being written.

2.5 _writeText

Required. Generates the plain-text representation of the property.

Method argument Description
MSAnniversary property The property object that is being written.
VCardVersion version The version of the vCard that is being generated.

Special characters: It is recommended that all special vCard characters be backslash-escaped when written to a plain-text vCard if they do not have special meanings within the property value. These special characters are: comma (,), semicolon (;), and backslash (\). The VCardPropertyScribe.escape() method can be used to do this. Newline escaping and line folding do not need to be handled here because they are handled in the VCardWriter class.

Our MSAnniversaryScribe class will produce a vCard property that looks something like this:

X-MS-ANNIVERSARY:20110301

2.6 _parseText

Required. Parses the plain-text representation of the property.

Method argument Description
String value The property value, as read off the wire.
VCardDataType dataType The property's data type. This will be either the value of the VALUE parameter, or the property's default data type if no VALUE parameter is present).
VCardVersion version The version of the vCard that is being parsed.
VCardParameters parameters The property's parameters. These parameters will be assigned to the property object when the _parseText method returns.
List<String> warnings Any non-critical parsing problems can be added to this list.

Special characters: It is recommended that you account for backslash-escaped characters when parsing a property value from a plain-text vCard. Characters that may be escaped include: comma (,), semicolon (;), and backslash (\). The VCardPropertyScribe.unescape() method can be used to do this. Newline unescaping and line unfolding do not need to be handled here because they are handled in the VCardReader class.

2.7 _writeXml

Optional. Generates the XML (xCard) representation of the property.

Method argument Description
MSAnniversary property The property object that is being written.
XCardElement element Represents the property's XML element. This class wraps xCard functionality around a raw org.w3c.dom.Element object. This object can be retrieved by calling XCardElement.element(), and can be modified as needed.

In our example, the XCardElement.append() method is used to add the xCard <date> element to the property's XML element.

Our MSAnniversaryScribe class will produce an XML element that looks something like this:

<x-ms-anniversary xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <date>20110301</date>
</x-ms-anniversary>

2.8 _parseXml

Optional. Parses the XML (xCard) representation of the property.

Method argument Description
XCardElement element Represents the property's XML element. This class wraps xCard functionality around a raw org.w3c.dom.Element object. This object can be retrieved by calling XCardElement.element().
VCardParameters parameters The property's parameters. These parameters will be assigned to the property object when the _parseXml method returns.
List<String> warnings Any non-critical parsing problems can be added to this list.

2.9 _parseHtml

Optional. Parses the HTML (hCard) representation of the property.

Method argument Description
HCardElement element Represents the property's HTML element.
List<String> warnings Any non-critical parsing problems can be added to this list.

2.10 _writeJson

Optional. Generates the JSON (jCard) representation of the property.

Method argument Description
MSAnniversary property The property object that is being written.

Our example uses the JCardValue.single() factory method to create a new JCardValue instance which contains a single value.

The JCardValue class contains the following factory methods to aid in the construction the three most typical types of jCard values.

  • single - Defines the value as a single value, such as a single string. Most properties are single valued.
  • multi - Defines the value as a list of values.
  • structured - Defines the value as a structured value (i.e. a "list of lists", such as the N property).

Objects may be passed into these methods. Primitive wrapper objects, such as Integer and Boolean, will be converted to their appropriate JSON data type. All other objects will be passed into the JSON stream as strings (their toString() method will be invoked). Null values will be converted to empty strings.

JCardValue value = JCardValue.single("one");
//yields: ["propName", {}, "text", "one"]
JCardValue value = JCardValue.multi("one", 2, true);
//yields: ["propName", {}, "text", "one", 2, true]
JCardValue value = JCardValue.structured(1, Arrays.asList(2, 3), 4);
//yields: ["propName", {}, "integer", [1, [2, 3], 4]]

Our MSAnniversary class will produce a jCard property that looks something like this:

["x-ms-anniversary", {}, "date", "20110301"]

2.11 _parseJson

Optional. Parses the JSON (jCard) representation of the property.

Method argument Description
JCardValue value Represents the property's jCard value.
VCardDataType dataType The property's data type.
VCardParameters parameters The property's parameters. These parameters will be assigned to the property object when the _parseJson method returns.
List<Warning> warnings Any non-critical parsing problems can be added to this list.

In our example, the JCardValue.asSingle() method is called to convert the raw JSON value to a single String value.

The JCardValue class has helper methods to aid in the retrieval of the three most typical types of jCard values.

  • asSingle - Gets the value of a property that contains a single value. Most properties are single valued.
  • asMulti - Gets the value of a property that contains multiple values.
  • asStructured - Gets the value of a property that contains a structured value (i.e. a "list of lists", such as the N property).

2.12 Exceptions

SkipMeException: Can be thrown from any of the parse or write methods if it is determined that the property should NOT be added to the VCard object (in the case of the parse methods) or should NOT be written to the data stream (in the case of the write methods). When this exception is thrown from a parse method, the exception message will be logged as a warning.

CannotParseException: Can be thrown from the parse methods if the property value cannot be unmarshalled. In our example, we throw this when the date string cannot be parsed into a Date object. This will cause the property to be unmarshalled as a RawProperty instead. RawProperties can be retrieved by calling the VCard.getExtendedProperty() method. This will also cause a warning will be added to the parser's warnings list.

3 Usage

3.1 Reading

Before a vCard is parsed, the property scribe must be registered with the reader object. Then, once a vCard has been read, the instances of the property can be retrieved using the VCard.getProperty(Class) or VCard.getProperties(Class) methods.

//using the Ezvcard class
Reader reader = ...
VCard vcard = Ezvcard.parse(reader).register(new MSAnniversaryScribe()).first();
reader.close();

MSAnniversary first = vcard.getProperty(MSAnniversary.class);
List<MSAnniversary> all = vcard.getProperties(MSAnniversary.class);
//using a reader class
Reader reader = ...
VCardReader vcr = new VCardReader(reader);
vcr.registerScribe(new MSAnniversaryScribe());
VCard vcard = vcr.readNext();
reader.close();

MSAnniversary first = vcard.getProperty(MSAnniversary.class);
List<MSAnniversary> all = vcard.getProperties(MSAnniversary.class);

3.2 Writing

To add an instance of a your property class to a VCard object, call the VCard.addProperty(VCardProperty) method. Then, register the property's scribe with the writer class before writing the vCard.

VCard vcard = new VCard();
MSAnniversary anniversary = new MSAnniversary(...);
vcard.addProperty(anniversary);

Writer writer = ...

//using the Ezvcard class
Ezvcard.write(vcard).register(new MSAnniversaryScribe()).go(writer);
writer.close();

//using writer class
VCardWriter vcw = new VCardWriter(writer);
vcw.registerScribe(new MSAnniversaryScribe());
vcw.write(vcard);
vcw.close();