Skip to content

Nonstandard Properties

Mike Angstadt edited this page Jun 24, 2017 · 2 revisions

iCalendar objects can contain non-standard properties that are not part of the iCal specification. These are called "experimental" properties.

The iCalendar object below contains an example of such a property. It is called "X-IMPORTANCE" (the names of experimental properties must begin with "X-").

BEGIN:VCALENDAR
PRODID:-//Company//Application//EN
VERSION:2.0
X-IMPORTANCE:1
END:VCALENDAR

1 Retrieving Experimental Properties

To get an experimental property, call the getExperimentalProperty(String) method. This returns the first property with that name in the form of a RawProperty object.

ICalendar ical = ...
RawProperty property = ical.getExperimentalProperty("X-IMPORTANCE");
String value = property.getValue();

If there is more than one instance of an experimental property, the getExperimentalProperties(String) method can be called to retrieve all instances:

ICalendar ical = ...
List<RawProperty> properties = ical.getExperimentalProperties("X-IMPORTANCE");
for (RawProperty property : properties){
  String value = property.getValue();
}

2 Creating Experimental Properties

To add an experimental property to an iCalendar object, call the addExperimentalProperty(String, String) method. The method (1) creates a RawProperty object, (2) adds it to the iCalendar object, and (3) returns the RawProperty object. Returning the created object allows you to assign parameters to the property, if necessary.

ICalendar ical = ...
RawProperty property = ical.addExperimentalProperty("X-IMPORTANCE", "1");
property.setParameter("X-FOO", "bar");

3 Creating a plugin

biweekly supports a plugin system that allows you to marshal and unmarshal experimental properties into Java objects. It requires the creation of a property class and a scribe class. This system can also be used to override the scribes of the standard properties, if need be.

The example below will create property and scribe classes for our "X-IMPORTANCE" example.

3.1 Property class

The property class extends ICalProperty. The property can hold either a text value (like "high" or "low") or a number (like "0" or "10").

public class Importance extends ICalProperty {
  private Integer number;
  private String text;

  public Importance(Integer value) {
    this.number = value;
  }

  public Importance(String text) {
    this.text = text;
  }

  public Integer getNumber() {
    return number;
  }

  public void setNumber(Integer number) {
    this.number = number;
    this.text = null;
  }

  public String getText() {
    return text;
  }

  public void setText(String text) {
    this.number = null;
    this.text = text;
  }

  //optional
  @Override
  protected void validate(List<ICalComponent> parentComponents, List<String> warnings) {
    if (number == null && text == null) {
      warnings.add("Value is null.");
    }

    if (number != null && number < 0) {
      warnings.add("Number cannot be less than 0");
    }
  }
}

The validate() method is optional. It validates the contents of the object when ICalendar.validate() is called. The first parameter, parentComponents, contains the hierarchy of components to which the property belongs. For example, if the property is inside of a VEVENT component, which is inside of an VCALENDAR component, index 0 of the list would be an ICalendar object and index 1 of the list would be an VEvent object (the first element of the list will always be an ICalendar object). The validation warnings are added to the warnings list.

3.2 Scribe class

The scribe class is responsible for reading/writing the property to/from the actual data stream (such as an ".ics" file). It extends the ICalPropertyScribe class.

public class ImportanceScribe extends ICalPropertyScribe<Importance> {
  public ImportanceScribe() {
    super(Importance.class, "X-IMPORTANCE", ICalDataType.INTEGER);
  }

  //optional
  //determines the iCal data type of the property's value
  @Override
  protected ICalDataType _dataType(Importance property) {
    if (property.getText() != null) {
      return ICalDataType.TEXT;
    }
    return ICalDataType.INTEGER;
  }

  //optional
  //tweaks the parameters before the property is written
  @Override
  protected void _prepareParameters(Importance property, ICalParameters copy) {
    Integer value = property.getNumber();
    if (value != null && value >= 10) {
      copy.put("X-MESSAGE", "very important!!");
    }
  }

  //required
  //writes the property to a plain-text iCal
  @Override
  protected String _writeText(Importance property) {
    return write(property);
  }

  //required
  //parses the property from a plain-text iCal
  @Override
  protected Importance _parseText(String value, ICalDataType dataType, ICalParameters parameters, List<String> warnings) {
    value = unescape(value);
    return parse(value, dataType);
  }

  //optional
  //writes the property to an XML document (xCal)
  @Override
  protected void _writeXml(Importance property, XCalElement element) {
    Integer value = property.getNumber();
    if (value != null) {
      if (value > 100) {
        throw new SkipMeException("Way too high.");
      }
      element.append(ICalDataType.INTEGER, value.toString()); //writes: <x-importance><integer>1</integer></x-importance>
      return;
    }

    String text = property.getText();
    if (text != null) {
      element.append(ICalDataType.TEXT, text); //writes: <x-importance><text>high</text></x-importance>
    }

  }

  //optional
  //reads the property from an XML document (xCal)
  @Override
  protected Importance _parseXml(XCalElement element, ICalParameters parameters, List<String> warnings) {
    String text = element.first(ICalDataType.TEXT);
    if (text != null) {
      return new Importance(text);
    }

    String number = element.first(ICalDataType.INTEGER);
    if (number != null) {
      try {
        return new Importance(Integer.valueOf(number));
      } catch (NumberFormatException e) {
        throw new CannotParseException("Numeric value expected: " + number);
      }
    }

    return new Importance(0);
  }

  //optional
  //writes the property to a JSON document (jCal)
  @Override
  protected JCalValue _writeJson(Importance property) {
    return JCalValue.single(write(property));
  }

  //optional
  //reads the property from a JSON document (jCal)
  @Override
  protected Importance _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, List<String> warnings) {
    String valueStr = value.asSingle();
    return parse(valueStr, dataType);
  }

  private Importance parse(String value, ICalDataType dataType) {
    if (dataType == ICalDataType.TEXT) {
      return new Importance(value);
    }

    try {
      return new Importance(Integer.valueOf(value));
    } catch (NumberFormatException e) {
      throw new CannotParseException("Numeric value expected: " + value);
    }
  }

  private String write(Importance property) {
    String text = property.getText();
    if (text != null) {
      return text;
    }

    Integer number = property.getNumber();
    if (number != null) {
      return number.toString();
    }

    return "";
  }
}

3.2.1 Constructor

The property class, the name of the property, and the property's default data type are passed into the parent constructor.

3.2.2 _prepareParameters

The _prepareParameters() method allows you to modify the property's parameters before the property is written. The second argument of the method is a copy of the property's parameters, and is the object to which you should make the modifications. A copy is made so that the property object itself does not get modified every time it is written.

3.2.3 _writeText

The _writeText() method marshals the property's value to a string for a plain-text iCalendar object.

A SkipMeException can be thrown if it is determined that the property should NOT be written out to the data stream.

3.2.4 _parseText

The _parseText() method is responsible for creating the Java object, based on what is read from a plain-text iCalendar data stream.

A SkipMeException can be thrown if it is determined that the property should NOT be added to the ICalendar Java object. The exception message will be logged as a warning. Warnings can be retrieved using the ICalReader.getWarnings() method.

A CannotParseException can be thrown if the property value cannot be parsed and a Java object cannot be created. When this happens, the property is instead unmarshalled as an experimental property, which means that its raw value can still be retrieved by calling the getExperimentalProperty(String) method on the component object that the property belongs to. The exception message will be logged as a warning. Warnings can be retrieved using the ICalReader.getWarnings() method.

3.2.4 _writeXml

The _writeXml() method marshals the property's value to an XML document (xCal standard). The XCalElement object is the element which contains the iCalendar property. It wraps a org.w3c.dom.Element object, adding xCal-specific functionality. The raw org.w3c.dom.Element object can be retrieved by calling XCalElement.getElement().

A SkipMeException can be thrown if it is determined that the property should NOT be written out to the data stream.

3.2.5 _parseXml

The _parseXml() method is responsible for creating the Java object, based on what is read from the XML document (xCal standard). The XCalElement object is the element which contains the iCalendar property. It wraps a org.w3c.dom.Element object, adding xCal-specific functionality. The raw org.w3c.dom.Element object can be retrieved by calling XCalElement.getElement().

The property's parameters are parsed before _parseXml() is called. They can be accessed from the ICalParameters object. Also, note that the XML elements that the parameters are stored in are removed from the XCalElement object.

A SkipMeException can be thrown if it is determined that the property should NOT be added to the ICalendar Java object. The exception message will be logged as a warning. Warnings can be retrieved using the ICalReader.getWarnings() method.

A CannotParseException can be thrown if the property value cannot be parsed and a Java object cannot be created. When this happens, the property is instead unmarshalled as an experimental property, which means that its raw value can still be retrieved by calling the getExperimentalProperty(String) method on the component object that the property belongs to. The exception message will be logged as a warning. Warnings can be retrieved using the ICalReader.getWarnings() method.

3.2.6 _writeJson

The _writeJson() method marshals the property's value to a JSON document (jCal standard). The method returns a JCalValue object, which represents the property's value and data type in JSON form.

The class has factory methods to aid in the construction the three most typical types of jCal 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 (such as the EXDATE property).
  • structured - Defines the value as a structured value (i.e. a "list of lists", such as the REQUEST-STATUS 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.

Examples:

JCalValue value = JCalValue.single("one");
//yields: ["propName", {}, "data-type", "one"]

JCalValue value = JCalValue.multi("one", 2, true);
//yields: ["propName", {}, "data-type", "one", 2, true]

JCalValue value = JCalValue.structured(1, Arrays.asList(2, 3), 4);
//yields: ["propName", {}, "data-type", [1, [2, 3], 4]]

A SkipMeException can be thrown if it is determined that the property should NOT be written out to the data stream.

3.2.7 _parseJson

The _parseXml() method is responsible for creating the Java object, based on what is read from the XML document (xCal standard). The XCalElement object is the element which contains the iCalendar property. It wraps a org.w3c.dom.Element object, adding xCal-specific functionality. The raw org.w3c.dom.Element object can be retrieved by calling XCalElement.getElement().

The _parseJson() method is responsible for creating the Java object, based on what is read from a JSON document (jCal standard). The JSON data is stored in the JCalValue object.

JCalValue has helper methods to aid in the retrieval the three most typical types of jCal 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 (such as the EXDATE property).
  • asStructured - Gets the value of a property that contains a structured value (i.e. a "list of lists", such as the REQUEST-STATUS property).

The property's parameters are parsed before _parseJson() is called. They can be accessed from the ICalParameters object.

A SkipMeException can be thrown if it is determined that the property should NOT be added to the ICalendar Java object. The exception message will be logged as a warning. Warnings can be retrieved using the JCalReader.getWarnings() method.

A CannotParseException can be thrown if the property value cannot be parsed and a Java object cannot be created. When this happens, the property is instead unmarshalled as an experimental property, which means that its raw value can still be retrieved by calling the getExperimentalProperty(String) method on the component object that the property belongs to. The exception message will be logged as a warning. Warnings can be retrieved using the JCalReader.getWarnings() method.

3.3 Plugin Usage

3.3.1 Reading

Before an iCalendar data stream is parsed, the scribe class must be registered with the reader object. Then, once an ICalendar object has been read, the instances of the property can be retrieved by calling the getProperty(Class) method.

String icalStr =
"BEGIN:VCALENDAR\r\n" +
  "PRODID:-//Company//Application//EN\r\n" +
  "VERSION:2.0\r\n" +
  "X-IMPORTANCE:1\r\n" +
"END:VCALENDAR\r\n";

//using "Biweekly" class
ICalendar ical = Biweekly.parse(icalStr)
                         .register(new ImportanceScribe())
                         .first();
Importance property = ical.getProperty(Importance.class);

//using "ICalReader" class
ICalReader icalReader = new ICalReader(icalStr);
icalReader.registerScribe(new ImportanceScribe());
ICalendar ical = icalReader.readNext();
Importance property = ical.getProperty(Importance.class);

3.3.2 Writing

To add instances of the property to the iCalendar object, call the addProperty() method on the component that the property belongs to. Then, register the scribe class with the writer object and perform the write operation.

ICalendar ical = new ICalendar();
Importance property = new Importance(1);
ical.addProperty(property);

//using "Biweekly" class
String icalStr = Biweekly.write(ical)
                         .register(new ImportanceScribe())
                         .go();

//using "ICalWriter" class
StringWriter sw = new StringWriter();
ICalWriter icalWriter = new ICalWriter(sw);
icalWriter.registerScribe(new ImportanceScribe());
icalWriter.write(ical);
String icalStr = sw.toString();