diff --git a/app/build.gradle b/app/build.gradle index f106c0084c..0a9769e4af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -665,9 +665,11 @@ dependencies { // https://github.com/mangstadt/biweekly // https://mvnrepository.com/artifact/net.sf.biweekly/biweekly - implementation("net.sf.biweekly:biweekly:$biweekly_version") { - exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' - } + //implementation("net.sf.biweekly:biweekly:$biweekly_version") { + // exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + //} + implementation "com.fasterxml.jackson.core:jackson-core:2.14.2" + implementation "com.fasterxml.jackson.core:jackson-databind:2.14.2" // https://github.com/mangstadt/ez-vcard implementation "com.googlecode.ez-vcard:ez-vcard:$vcard_version" diff --git a/app/src/main/java/biweekly/Biweekly.java b/app/src/main/java/biweekly/Biweekly.java new file mode 100644 index 0000000000..65377d65e3 --- /dev/null +++ b/app/src/main/java/biweekly/Biweekly.java @@ -0,0 +1,443 @@ +package biweekly; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Arrays; +import java.util.Collection; +import java.util.Properties; + +import org.w3c.dom.Document; + +import biweekly.io.chain.ChainingJsonParser; +import biweekly.io.chain.ChainingJsonStringParser; +import biweekly.io.chain.ChainingJsonWriter; +import biweekly.io.chain.ChainingTextParser; +import biweekly.io.chain.ChainingTextStringParser; +import biweekly.io.chain.ChainingTextWriter; +import biweekly.io.chain.ChainingXmlMemoryParser; +import biweekly.io.chain.ChainingXmlParser; +import biweekly.io.chain.ChainingXmlWriter; +import biweekly.io.json.JCalReader; +import biweekly.io.json.JCalWriter; +import biweekly.io.text.ICalReader; +import biweekly.io.text.ICalWriter; +import biweekly.io.xml.XCalDocument; +import biweekly.util.IOUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Contains static chaining factory methods for reading/writing iCalendar + * objects. + *

+ * + *

+ * Writing an iCalendar object + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * //string
+ * String icalString = Biweekly.write(ical).go();
+ * 
+ * //file
+ * File file = new File("meeting.ics");
+ * Biweekly.write(ical).go(file);
+ * 
+ * //output stream
+ * OutputStream out = ...
+ * Biweekly.write(ical).go(out);
+ * out.close();
+ * 
+ * //writer (should be configured to use UTF-8 encoding)
+ * Writer writer = ...
+ * Biweekly.write(ical).go(writer);
+ * writer.close();
+ * 
+ * + *

+ * Writing multiple iCalendar objects + *

+ * + *
+ * ICalendar ical1 = new ICalendar();
+ * ICalendar ical2 = new ICalendar();
+ * 
+ * String icalString = Biweekly.write(ical1, ical2).go();
+ * 
+ * + *

+ * Writing an XML-encoded iCalendar object (xCal) + *

+ * + *
+ * //Call writeXml() instead of write()
+ * ICalendar ical = new ICalendar();
+ * String xml = Biweekly.writeXml(ical).indent(2).go();
+ * 
+ * + *

+ * Writing a JSON-encoded iCalendar object (jCal) + *

+ * + *
+ * //Call writeJson() instead of write()
+ * ICalendar ical = new ICalendar();
+ * String json = Biweekly.writeJson(ical).go();
+ * 
+ * + *

+ * Reading an iCalendar object + *

+ * + *
+ * ICalendar ical;
+ * 
+ * //string
+ * String icalStr = ...
+ * ical = Biweekly.parse(icalStr).first();
+ * 
+ * //file
+ * File file = new File("meeting.ics");
+ * ical = Biweekly.parse(file).first();
+ * 
+ * //input stream
+ * InputStream in = ...
+ * ical = Biweekly.parse(in).first();
+ * in.close();  
+ * 
+ * //reader (should be configured to read UTF-8)
+ * Reader reader = ...
+ * ical = Biweekly.parse(reader).first();
+ * reader.close();
+ * 
+ * + *

+ * Reading multiple iCalendar objects + *

+ * + *
+ * String icalStr = ...
+ * List<ICalendar> icals = Biweekly.parse(icalStr).all();
+ * 
+ * + *

+ * Reading an XML-encoded iCalendar object (xCal) + *

+ * + *
+ * //Call parseXml() instead of parse()
+ * String xml = ...
+ * ICalendar ical = Biweekly.parseXml(xml).first();
+ * 
+ * + *

+ * Reading a JSON-encoded iCalendar object (Cal) + *

+ * + *
+ * //Call parseJson() instead of parse()
+ * String json = ...
+ * ICalendar ical = Biweekly.parseJson(json).first();
+ * 
+ * + *

+ * Retrieving parser warnings + *

+ * + *
+ * String icalStr = ...
+ * List<List<String>> warnings = new ArrayList<List<String>>();
+ * List<ICalendar> icals = Biweekly.parse(icalStr).warnings(warnings).all();
+ * int i = 0;
+ * for (List<String> icalWarnings : warnings) {
+ *   System.out.println("iCal #" + (i++) + " warnings:");
+ *   for (String warning : icalWarnings) {
+ *     System.out.println(warning);
+ *   }
+ * }
+ * 
+ * + *

+ * The methods in this class make use of the following classes. These classes + * can be used if greater control over the read/write operation is required: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Classes used by this class
ClassesSupports
+ * streaming?
Text{@link ICalReader} / {@link ICalWriter}yes
XML{@link XCalDocument}no
JSON{@link JCalReader} / {@link JCalWriter}yes
+ * @author Michael Angstadt + */ +public final class Biweekly { + /** + * The version of the library. + */ + public static final String VERSION; + + /** + * The Maven group ID. + */ + public static final String GROUP_ID; + + /** + * The Maven artifact ID. + */ + public static final String ARTIFACT_ID; + + /** + * The project webpage. + */ + public static final String URL; + + static { + InputStream in = null; + try { + in = Biweekly.class.getResourceAsStream("biweekly.properties"); + Properties props = new Properties(); + props.load(in); + + VERSION = props.getProperty("version"); + GROUP_ID = props.getProperty("groupId"); + ARTIFACT_ID = props.getProperty("artifactId"); + URL = props.getProperty("url"); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Parses an iCalendar object string. + * @param ical the iCalendar data + * @return chainer object for completing the parse operation + */ + public static ChainingTextStringParser parse(String ical) { + return new ChainingTextStringParser(ical); + } + + /** + * Parses an iCalendar file. + * @param file the iCalendar file + * @return chainer object for completing the parse operation + */ + public static ChainingTextParser> parse(File file) { + return new ChainingTextParser>(file); + } + + /** + * Parses an iCalendar data stream. + * @param in the input stream + * @return chainer object for completing the parse operation + */ + public static ChainingTextParser> parse(InputStream in) { + return new ChainingTextParser>(in); + } + + /** + * Parses an iCalendar data stream. + * @param reader the reader + * @return chainer object for completing the parse operation + */ + public static ChainingTextParser> parse(Reader reader) { + return new ChainingTextParser>(reader); + } + + /** + * Writes multiple iCalendar objects to a data stream. + * @param icals the iCalendar objects to write + * @return chainer object for completing the write operation + */ + public static ChainingTextWriter write(ICalendar... icals) { + return write(Arrays.asList(icals)); + } + + /** + * Writes multiple iCalendar objects to a data stream. + * @param icals the iCalendar objects to write + * @return chainer object for completing the write operation + */ + public static ChainingTextWriter write(Collection icals) { + return new ChainingTextWriter(icals); + } + + /** + * Parses an xCal document (XML-encoded iCalendar objects) from a string. + * @param xml the XML string + * @return chainer object for completing the parse operation + */ + public static ChainingXmlMemoryParser parseXml(String xml) { + return new ChainingXmlMemoryParser(xml); + } + + /** + * Parses an xCal document (XML-encoded iCalendar objects) from a file. + * @param file the XML file + * @return chainer object for completing the parse operation + */ + public static ChainingXmlParser> parseXml(File file) { + return new ChainingXmlParser>(file); + } + + /** + * Parses an xCal document (XML-encoded iCalendar objects) from an input + * stream. + * @param in the input stream + * @return chainer object for completing the parse operation + */ + public static ChainingXmlParser> parseXml(InputStream in) { + return new ChainingXmlParser>(in); + } + + /** + *

+ * Parses an xCal document (XML-encoded iCalendar objects) from a reader. + *

+ *

+ * Note that use of this method is discouraged. It ignores the character + * encoding that is defined within the XML document itself, and should only + * be used if the encoding is undefined or if the encoding needs to be + * ignored for whatever reason. The {@link #parseXml(InputStream)} method + * should be used instead, since it takes the XML document's character + * encoding into account when parsing. + *

+ * @param reader the reader + * @return chainer object for completing the parse operation + */ + public static ChainingXmlParser> parseXml(Reader reader) { + return new ChainingXmlParser>(reader); + } + + /** + * Parses an xCal document (XML-encoded iCalendar objects). + * @param document the XML document + * @return chainer object for completing the parse operation + */ + public static ChainingXmlMemoryParser parseXml(Document document) { + return new ChainingXmlMemoryParser(document); + } + + /** + * Writes an xCal document (XML-encoded iCalendar objects). + * @param icals the iCalendar object(s) to write + * @return chainer object for completing the write operation + */ + public static ChainingXmlWriter writeXml(ICalendar... icals) { + return writeXml(Arrays.asList(icals)); + } + + /** + * Writes an xCal document (XML-encoded iCalendar objects). + * @param icals the iCalendar objects to write + * @return chainer object for completing the write operation + */ + public static ChainingXmlWriter writeXml(Collection icals) { + return new ChainingXmlWriter(icals); + } + + /** + * Parses a jCal data stream (JSON-encoded iCalendar objects). + * @param json the JSON data + * @return chainer object for completing the parse operation + */ + public static ChainingJsonStringParser parseJson(String json) { + return new ChainingJsonStringParser(json); + } + + /** + * Parses a jCal data stream (JSON-encoded iCalendar objects). + * @param file the JSON file + * @return chainer object for completing the parse operation + */ + public static ChainingJsonParser> parseJson(File file) { + return new ChainingJsonParser>(file); + } + + /** + * Parses a jCal data stream (JSON-encoded iCalendar objects). + * @param in the input stream + * @return chainer object for completing the parse operation + */ + public static ChainingJsonParser> parseJson(InputStream in) { + return new ChainingJsonParser>(in); + } + + /** + * Parses a jCal data stream (JSON-encoded iCalendar objects). + * @param reader the reader + * @return chainer object for completing the parse operation + */ + public static ChainingJsonParser> parseJson(Reader reader) { + return new ChainingJsonParser>(reader); + } + + /** + * Writes an xCal document (XML-encoded iCalendar objects). + * @param icals the iCalendar object(s) to write + * @return chainer object for completing the write operation + */ + public static ChainingJsonWriter writeJson(ICalendar... icals) { + return writeJson(Arrays.asList(icals)); + } + + /** + * Writes an xCal document (XML-encoded iCalendar objects). + * @param icals the iCalendar objects to write + * @return chainer object for completing the write operation + */ + public static ChainingJsonWriter writeJson(Collection icals) { + return new ChainingJsonWriter(icals); + } + + private Biweekly() { + //hide + } +} diff --git a/app/src/main/java/biweekly/ICalDataType.java b/app/src/main/java/biweekly/ICalDataType.java new file mode 100644 index 0000000000..d0960ca94b --- /dev/null +++ b/app/src/main/java/biweekly/ICalDataType.java @@ -0,0 +1,222 @@ +package biweekly; + +import java.util.Collection; + +import biweekly.util.CaseClasses; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the data type of a property's value. + * @author Michael Angstadt + * @see RFC 5545 + * p.29-50 + */ +public class ICalDataType { + private static final CaseClasses enums = new CaseClasses(ICalDataType.class) { + @Override + protected ICalDataType create(String value) { + return new ICalDataType(value); + } + + @Override + protected boolean matches(ICalDataType dataType, String value) { + return dataType.name.equalsIgnoreCase(value); + } + }; + + /** + * Binary data (such as an image or word-processing document). + * @see RFC 5545 + * p.30-1 + * @see vCal 1.0 p.18 + */ + public static final ICalDataType BINARY = new ICalDataType("BINARY"); + + /** + * Boolean value ("true" or "false"). + * @see RFC 5545 + * p.31 + */ + public static final ICalDataType BOOLEAN = new ICalDataType("BOOLEAN"); + + /** + * A URI containing a calendar user address (typically, a "mailto" URI). + * @see RFC 5545 + * p.30-1 + */ + public static final ICalDataType CAL_ADDRESS = new ICalDataType("CAL-ADDRESS"); + + /** + * The property value is located in a separate MIME entity (vCal 1.0 only). + * @see vCal 1.0 p.17 + */ + public static final ICalDataType CONTENT_ID = new ICalDataType("CONTENT-ID"); //1.0 only + + /** + * A date (for example, "2014-03-12"). + * @see RFC 5545 + * p.32 + * @see vCal 1.0 p.16-7 + */ + public static final ICalDataType DATE = new ICalDataType("DATE"); + + /** + * A date/time value (for example, "2014-03-12 13:30:00"). + * @see RFC 5545 + * p.32-4 + * @see vCal 1.0 p.16-7 + */ + public static final ICalDataType DATE_TIME = new ICalDataType("DATE-TIME"); + + /** + * A duration of time (for example, "2 hours, 30 minutes"). + * @see RFC 5545 + * p.35-6 + * @see vCal 1.0 p.17 + */ + public static final ICalDataType DURATION = new ICalDataType("DURATION"); + + /** + * A floating point value (for example, "3.14") + * @see RFC 5545 + * p.36 + */ + public static final ICalDataType FLOAT = new ICalDataType("FLOAT"); + + /** + * An integer value (for example, "42") + * @see RFC 5545 + * p.37 + */ + public static final ICalDataType INTEGER = new ICalDataType("INTEGER"); + + /** + * A period of time (for example, "October 3 through October 5"). + * @see RFC 5545 + * p.37-8 + */ + public static final ICalDataType PERIOD = new ICalDataType("PERIOD"); + + /** + * A recurrence rule (for example, "every Monday at 2pm"). + * @see RFC 5545 + * p.38-45 + * @see vCal 1.0 p.18-23 + */ + public static final ICalDataType RECUR = new ICalDataType("RECUR"); + + /** + * A plain text value. + * @see RFC 5545 + * p.45-6 + */ + public static final ICalDataType TEXT = new ICalDataType("TEXT"); + + /** + * A time value (for example, "2pm"). + * @see RFC 5545 + * p.47-8 + */ + public static final ICalDataType TIME = new ICalDataType("TIME"); + + /** + * A URI value. + * @see RFC 5545 + * p.49 + */ + public static final ICalDataType URI = new ICalDataType("URI"); + + /** + * A URL (for example, "http://example.com/picture.jpg") (vCal 1.0 only). + * @see vCal 1.0 p.17-8 + */ + public static final ICalDataType URL = new ICalDataType("URL"); + + /** + * A UTC-offset (for example, "+0500"). + * @see RFC 5545 + * p.49-50 + */ + public static final ICalDataType UTC_OFFSET = new ICalDataType("UTC-OFFSET"); + + private final String name; + + private ICalDataType(String name) { + this.name = name; + } + + /** + * Gets the name of the data type. + * @return the name of the data type (e.g. "TEXT") + */ + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static ICalDataType find(String value) { + if ("CID".equalsIgnoreCase(value)) { + //"CID" is an alias for "CONTENT-ID" (vCal 1.0, p.17) + return CONTENT_ID; + } + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static ICalDataType get(String value) { + if ("CID".equalsIgnoreCase(value)) { + //"CID" is an alias for "CONTENT-ID" (vCal 1.0, p.17) + return CONTENT_ID; + } + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/ICalVersion.java b/app/src/main/java/biweekly/ICalVersion.java new file mode 100644 index 0000000000..885f4ae609 --- /dev/null +++ b/app/src/main/java/biweekly/ICalVersion.java @@ -0,0 +1,104 @@ +package biweekly; + +import com.github.mangstadt.vinnie.SyntaxStyle; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines all supported versions of the iCalendar standard. + * @author Michael Angstadt + */ +public enum ICalVersion { + /** + * The original vCalendar specification. + * @see 1.0 specs + */ + V1_0("1.0", SyntaxStyle.OLD), + + /** + * An older, deprecated version of the iCalendar specification (very similar + * to {@link #V2_0}). + * @see RFC 2445 + */ + V2_0_DEPRECATED("2.0", SyntaxStyle.NEW), + + /** + * The latest iCalendar specification. + * @see RFC 5545 + */ + V2_0("2.0", SyntaxStyle.NEW); + + private final String version; + private final SyntaxStyle syntaxStyle; + + /** + * @param version the version number + */ + ICalVersion(String version, SyntaxStyle syntaxStyle) { + this.version = version; + this.syntaxStyle = syntaxStyle; + } + + /** + * Gets the text representation of this version. + * @return the text representation + */ + public String getVersion() { + return version; + } + + /** + * Gets the syntax style used by this version when writing to a plain-text + * data stream. + * @return the syntax style + */ + public SyntaxStyle getSyntaxStyle() { + return syntaxStyle; + } + + /** + * Gets a {@link ICalVersion} instance based on the given version number. + * @param version the version number (e.g. "2.0") + * @return the object or null if not found + */ + public static ICalVersion get(String version) { + if (V1_0.version.equals(version)) { + return V1_0; + } + if (V2_0.version.equals(version)) { + return V2_0; + } + return null; + } + + @Override + public String toString() { + if (this == V2_0_DEPRECATED) { + return version + " (obsoleted)"; + } + return version; + } +} diff --git a/app/src/main/java/biweekly/ICalendar.java b/app/src/main/java/biweekly/ICalendar.java new file mode 100644 index 0000000000..0fdb72e1a4 --- /dev/null +++ b/app/src/main/java/biweekly/ICalendar.java @@ -0,0 +1,1263 @@ +package biweekly; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.transform.TransformerException; + +import biweekly.ValidationWarnings.WarningsGroup; +import biweekly.component.ICalComponent; +import biweekly.component.VEvent; +import biweekly.component.VFreeBusy; +import biweekly.component.VJournal; +import biweekly.component.VTodo; +import biweekly.io.TimezoneInfo; +import biweekly.io.json.JCalWriter; +import biweekly.io.text.ICalWriter; +import biweekly.io.xml.XCalDocument; +import biweekly.io.xml.XCalWriter; +import biweekly.property.CalendarScale; +import biweekly.property.Categories; +import biweekly.property.Color; +import biweekly.property.Description; +import biweekly.property.Geo; +import biweekly.property.ICalProperty; +import biweekly.property.Image; +import biweekly.property.LastModified; +import biweekly.property.Method; +import biweekly.property.Name; +import biweekly.property.ProductId; +import biweekly.property.RefreshInterval; +import biweekly.property.Source; +import biweekly.property.Uid; +import biweekly.property.Url; +import biweekly.util.Duration; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Represents an iCalendar object. + *

+ * + *

+ * Examples: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * VEvent event = new VEvent();
+ * event.setSummary("Team Meeting");
+ * Date start = ...;
+ * event.setDateStart(start);
+ * Date end = ...;
+ * event.setDateEnd(end);
+ * ical.addEvent(event);
+ * 
+ * + *

+ * Getting timezone information from parsed iCalendar objects: + *

+ * + *
+ * //The timezone information associated with an ICalendar object is stored in its TimezoneInfo object.
+ * ICalReader reader = ...
+ * ICalendar ical = reader.readNext();
+ * TimezoneInfo tzinfo = ical.getTimezoneInfo();
+ * 
+ * //You can use this object to get the VTIMEZONE components that were parsed from the input stream.
+ * //Note that the VTIMEZONE components will NOT be in the ICalendar object itself
+ * Collection<VTimezone> vtimezones = tzinfo.getComponents();
+ * 
+ * //You can also get the timezone that a specific property was originally formatted in.
+ * DateStart dtstart = ical.getEvents().get(0).getDateStart();
+ * TimeZone tz = tzinfo.getTimezone(dtstart).getTimeZone();
+ * 
+ * //This is useful for calculating recurrence rule dates.
+ * RecurrenceRule rrule = ical.getEvents(0).getRecurrenceRule();
+ * DateIterator it = rrule.getDateIterator(dtstart.getValue(), tz);
+ * 
+ * + *

+ * Setting timezone information when writing iCalendar objects: + *

+ * + *
+ * //The TimezoneInfo field is used to determine what timezone to format each date-time value in when the ICalendar object is written.
+ * //Appropriate VTIMEZONE components are automatically added to the written iCalendar object.
+ * ICalendar ical = ...
+ * TimezoneInfo tzinfo = ical.getTimezoneInfo();
+ * 
+ * //biweekly uses the TimezoneAssignment class to define timezones.
+ * //This class groups together a Java TimeZone object, which is used to format/parse the date-time values, and its equivalent VTIMEZONE component definition.
+ * 
+ * //biweekly can auto-generate the VTIMEZONE definitions by downloading them from tzurl.org.
+ * //If you want the generated VTIMEZONE components to be tailored for Microsoft Outlook email clients, pass "true" into this method.
+ * TimezoneAssignment timezone = TimezoneAssignment.download(TimeZone.getTimeZone("America/New_York"), true);
+ * 
+ * //Using the TimezoneAssignment class, you can specify what timezone you'd like to format all date-time values in.
+ * tzinfo.setDefaultTimezone(timezone);
+ * 
+ * //You can also specify what timezone to use for individual properties if you want.
+ * DateStart dtstart = ical.getEvents(0).getDateStart();
+ * TimezoneAssignment losAngeles = TimezoneAssignment.download(TimeZone.getTimeZone("America/Los_Angeles"), true);
+ * tzinfo.setTimezone(dtstart, losAngeles);
+ * 
+ * //The writer object will use this information to determine what timezone to format each date-time value in.
+ * //Date-time values are formatted in UTC by default.
+ * ICalWriter writer = ...
+ * writer.write(ical);
+ * 
+ * + *

+ * For more information on working with timezones, see this page: https://github. + * com/mangstadt/biweekly/wiki/Timezones + *

+ * @author Michael Angstadt + * @see RFC 5545 + * @see RFC 2445 + * @see vCal 1.0 + * @see draft-ietf-calext-extensions-01 + */ +public class ICalendar extends ICalComponent { + private ICalVersion version; + private TimezoneInfo tzinfo = new TimezoneInfo(); + + /** + *

+ * Creates a new iCalendar object. + *

+ *

+ * The following properties are added to the component when it is created: + *

+ *
    + *
  • {@link ProductId}: Set to a value that represents this library.
  • + *
+ */ + public ICalendar() { + setProductId(ProductId.biweekly()); + } + + /** + * Copy constructor. + * @param original the iCalendar object to make a copy of + */ + public ICalendar(ICalendar original) { + super(original); + version = original.version; + } + + /** + * Gets the version of this iCalendar object. + * @return the version + */ + public ICalVersion getVersion() { + return version; + } + + /** + * Sets the version of this iCalendar object. + * @param version the version + */ + public void setVersion(ICalVersion version) { + this.version = version; + } + + /** + *

+ * Gets the timezone information associated with this iCalendar object. + *

+ *

+ * When an iCalendar object is parsed from an input stream, the + * {@link TimezoneInfo} object remembers the original timezone definitions + * that each property was associated with. One use for this is when you want + * to calculate the dates in a recurrence rule. The recurrence rule needs to + * know what timezone its associated date values were originally formatted + * in in order to work correctly. + *

+ *

+ * When an {@link ICalendar} object is written to an output stream, its + * {@link TimezoneInfo} object tells the writer what timezone to format each + * property in. + *

+ * @return the timezone info + */ + public TimezoneInfo getTimezoneInfo() { + return tzinfo; + } + + /** + *

+ * Sets the timezone information associated with this iCalendar object. + *

+ *

+ * When an iCalendar object is parsed from an input stream, the + * {@link TimezoneInfo} object remembers the original timezone definitions + * that each property was associated with. One use for this is when you want + * to calculate the dates in a recurrence rule. The recurrence rule needs to + * know what timezone its associated date values were originally formatted + * in in order to work correctly. + *

+ *

+ * When an {@link ICalendar} object is written to an output stream, its + * {@link TimezoneInfo} object tells the writer what timezone to format each + * property in. + *

+ * @param tzinfo the timezone info (cannot be null) + * @throws NullPointerException if the timezone info object is null + */ + public void setTimezoneInfo(TimezoneInfo tzinfo) { + if (tzinfo == null) { + throw new NullPointerException(); + } + this.tzinfo = tzinfo; + } + + /** + * Gets the name of the application that created the iCalendar object. All + * {@link ICalendar} objects are initialized with a product ID representing + * this library. + * @return the property instance or null if not set + * @see RFC 5545 + * p.78-9 + * @see RFC 2445 + * p.75-6 + * @see vCal 1.0 p.24 + */ + public ProductId getProductId() { + return getProperty(ProductId.class); + } + + /** + * Sets the name of the application that created the iCalendar object. All + * {@link ICalendar} objects are initialized with a product ID representing + * this library. + * @param prodId the property instance or null to remove + * @see RFC 5545 + * p.78-9 + * @see RFC 2445 + * p.75-6 + * @see vCal 1.0 p.24 + */ + public void setProductId(ProductId prodId) { + setProperty(ProductId.class, prodId); + } + + /** + * Sets the application that created the iCalendar object. All + * {@link ICalendar} objects are initialized with a product ID representing + * this library. + * @param prodId a unique string representing the application (e.g. + * "-//Company//Application//EN") or null to remove + * @return the property that was created + * @see RFC 5545 + * p.78-9 + * @see RFC 2445 + * p.75-6 + * @see vCal 1.0 p.24 + */ + public ProductId setProductId(String prodId) { + ProductId property = (prodId == null) ? null : new ProductId(prodId); + setProductId(property); + return property; + } + + /** + * Gets the calendar system that this iCalendar object uses. If none is + * specified, then the calendar is assumed to be in Gregorian format. + * @return the calendar system or null if not set + * @see RFC 5545 + * p.76-7 + * @see RFC 2445 + * p.73-4 + */ + public CalendarScale getCalendarScale() { + return getProperty(CalendarScale.class); + } + + /** + * Sets the calendar system that this iCalendar object uses. If none is + * specified, then the calendar is assumed to be in Gregorian format. + * @param calendarScale the calendar system or null to remove + * @see RFC 5545 + * p.76-7 + * @see RFC 2445 + * p.73-4 + */ + public void setCalendarScale(CalendarScale calendarScale) { + setProperty(CalendarScale.class, calendarScale); + } + + /** + * Gets the type of iTIP + * request that this iCalendar object represents, or the value of the + * "Content-Type" header's "method" parameter if the iCalendar object is + * defined as a MIME message entity. + * @return the property or null if not set + * @see RFC 5546 + * @see RFC 5545 + * p.77-8 + * @see RFC 2445 + * p.74-5 + */ + public Method getMethod() { + return getProperty(Method.class); + } + + /** + * Sets the type of iTIP + * request that this iCalendar object represents, or the value of the + * "Content-Type" header's "method" parameter if the iCalendar object is + * defined as a MIME message entity. + * @param method the property or null to remove + * @see RFC 5546 + * @see RFC 5545 + * p.77-8 + * @see RFC 2445 + * p.74-5 + */ + public void setMethod(Method method) { + setProperty(Method.class, method); + } + + /** + * Sets the type of iTIP + * request that this iCalendar object represents, or the value of the + * "Content-Type" header's "method" parameter if the iCalendar object is + * defined as a MIME message entity. + * @param method the method or null to remove + * @return the property that was created + * @see RFC 5546 + * @see RFC 5545 + * p.77-8 + * @see RFC 2445 + * p.74-5 + */ + public Method setMethod(String method) { + Method property = (method == null) ? null : new Method(method); + setMethod(property); + return property; + } + + /** + *

+ * Gets the human-readable name of the calendar as a whole. + *

+ *

+ * An iCalendar object can only have one name, but multiple {@link Name} + * properties can exist in order to specify the name in multiple languages. + * In this case, each property instance must be assigned a LANGUAGE + * parameter. + *

+ * @return the names (any changes made this list will affect the parent + * component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.5 + */ + public List getNames() { + return getProperties(Name.class); + } + + /** + * Sets the human-readable name of the calendar as a whole. + * @param name the name or null to remove + * @see draft-ietf-calext-extensions-01 + * p.5 + */ + public void setName(Name name) { + setProperty(Name.class, name); + } + + /** + * Sets the human-readable name of the calendar as a whole. + * @param name the name or null to remove + * @return the property that was created + * @see draft-ietf-calext-extensions-01 + * p.5 + */ + public Name setName(String name) { + Name property = (name == null) ? null : new Name(name); + setName(property); + return property; + } + + /** + *

+ * Assigns a human-readable name to the calendar as a whole. + *

+ *

+ * An iCalendar object can only have one name, but multiple {@link Name} + * properties can exist in order to specify the name in multiple languages. + * In this case, each property instance must be assigned a LANGUAGE + * parameter. + *

+ * @param name the name + * @see draft-ietf-calext-extensions-01 + * p.5 + */ + public void addName(Name name) { + addProperty(name); + } + + /** + *

+ * Assigns a human-readable name to the calendar as a whole. + *

+ *

+ * An iCalendar object can only have one name, but multiple {@link Name} + * properties can exist in order to specify the name in multiple languages. + * In this case, each property instance must be assigned a LANGUAGE + * parameter. + *

+ * @param name the name (e.g. "Company Vacation Days") + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.5 + */ + public Name addName(String name) { + Name property = new Name(name); + addProperty(property); + return property; + } + + /** + *

+ * Gets the human-readable description of the calendar as a whole. + *

+ *

+ * An iCalendar object can only have one description, but multiple + * {@link Description} properties can exist in order to specify the + * description in multiple languages. In this case, each property instance + * must be assigned a LANGUAGE parameter. + *

+ * @return the descriptions (any changes made this list will affect the + * parent component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public List getDescriptions() { + return getProperties(Description.class); + } + + /** + * Sets the human-readable description of the calendar as a whole. + * @param description the description or null to remove + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public void setDescription(Description description) { + setProperty(Description.class, description); + } + + /** + * Sets the human-readable description of the calendar as a whole. + * @param description the description or null to remove + * @return the property that was created + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public Description setDescription(String description) { + Description property = (description == null) ? null : new Description(description); + setDescription(property); + return property; + } + + /** + *

+ * Assigns a human-readable description to the calendar as a whole. + *

+ *

+ * An iCalendar object can only have one description, but multiple + * {@link Description} properties can exist in order to specify the + * description in multiple languages. In this case, each property instance + * must be assigned a LANGUAGE parameter. + *

+ * @param description the description + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public void addDescription(Description description) { + addProperty(description); + } + + /** + *

+ * Assigns a human-readable description to the calendar as a whole. + *

+ *

+ * An iCalendar object can only have one description, but multiple + * {@link Description} properties can exist in order to specify the + * description in multiple languages. In this case, each property instance + * must be assigned a LANGUAGE parameter. + *

+ * @param description the description + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public Description addDescription(String description) { + Description property = new Description(description); + addProperty(property); + return property; + } + + /** + * Gets the calendar's unique identifier. + * @return the unique identifier or null if not set + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public Uid getUid() { + return getProperty(Uid.class); + } + + /** + * Sets the calendar's unique identifier. + * @param uid the unique identifier or null to remove + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public void setUid(Uid uid) { + setProperty(Uid.class, uid); + } + + /** + * Sets the calendar's unique identifier. + * @param uid the unique identifier or null to remove + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.6 + */ + public Uid setUid(String uid) { + Uid property = (uid == null) ? null : new Uid(uid); + setUid(property); + return property; + } + + /** + * Gets the date and time that the information in this calendar object was + * last revised. + * @return the last modified time or null if not set + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public LastModified getLastModified() { + return getProperty(LastModified.class); + } + + /** + * Sets the date and time that the information in this calendar object was + * last revised. + * @param lastModified the last modified time or null to remove + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public void setLastModified(LastModified lastModified) { + setProperty(LastModified.class, lastModified); + } + + /** + * Sets the date and time that the information in this calendar object was + * last revised. + * @param lastModified the date and time or null to remove + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public LastModified setLastModified(Date lastModified) { + LastModified property = (lastModified == null) ? null : new LastModified(lastModified); + setLastModified(property); + return property; + } + + /** + * Gets the location of a more dynamic, alternate representation of the + * calendar (such as a website that allows you to interact with the calendar + * data). + * @return the URL or null if not set + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public Url getUrl() { + return getProperty(Url.class); + } + + /** + * Sets the location of a more dynamic, alternate representation of the + * calendar (such as a website that allows you to interact with the calendar + * data). + * @param url the URL or null to remove + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public void setUrl(Url url) { + setProperty(Url.class, url); + } + + /** + * Sets the location of a more dynamic, alternate representation of the + * calendar (such as a website that allows you to interact with the calendar + * data). + * @param url the URL or null to remove + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public Url setUrl(String url) { + Url property = (url == null) ? null : new Url(url); + setUrl(property); + return property; + } + + /** + * Gets the keywords that describe the calendar. + * @return the categories (any changes made this list will affect the parent + * component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public List getCategories() { + return getProperties(Categories.class); + } + + /** + * Adds a list of keywords that describe the calendar. + * @param categories the categories to add + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public void addCategories(Categories categories) { + addProperty(categories); + } + + /** + * Adds a list of keywords that describe the calendar. + * @param categories the categories to add + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public Categories addCategories(String... categories) { + Categories prop = new Categories(categories); + addProperty(prop); + return prop; + } + + /** + * Gets the suggested minimum polling interval for checking for updates to + * the calendar data. + * @return the refresh interval or null if not set + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public RefreshInterval getRefreshInterval() { + return getProperty(RefreshInterval.class); + } + + /** + * Sets the suggested minimum polling interval for checking for updates to + * the calendar data. + * @param refreshInterval the refresh interval or null to remove + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public void setRefreshInterval(RefreshInterval refreshInterval) { + setProperty(RefreshInterval.class, refreshInterval); + } + + /** + * Sets the suggested minimum polling interval for checking for updates to + * the calendar data. + * @param refreshInterval the refresh interval or null to remove + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.7 + */ + public RefreshInterval setRefreshInterval(Duration refreshInterval) { + RefreshInterval property = (refreshInterval == null) ? null : new RefreshInterval(refreshInterval); + setRefreshInterval(property); + return property; + } + + /** + * Gets the location that the calendar data can be refreshed from. + * @return the source or null if not set + * @see draft-ietf-calext-extensions-01 + * p.8 + */ + public Source getSource() { + return getProperty(Source.class); + } + + /** + * Sets the location that the calendar data can be refreshed from. + * @param source the source or null to remove + * @see draft-ietf-calext-extensions-01 + * p.8 + */ + public void setSource(Source source) { + setProperty(Source.class, source); + } + + /** + * Sets the location that the calendar data can be refreshed from. + * @param url the source or null to remove + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.8 + */ + public Source setSource(String url) { + Source property = (url == null) ? null : new Source(url); + setSource(property); + return property; + } + + /** + * Gets the color that clients may use when displaying the calendar (for + * example, a background color). + * @return the color or null if not set + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color getColor() { + return getProperty(Color.class); + } + + /** + * Sets the color that clients may use when displaying the calendar (for + * example, a background color). + * @param color the color or null to remove + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public void setColor(Color color) { + setProperty(Color.class, color); + } + + /** + * Sets the color that clients may use when displaying the calendar (for + * example, a background color). + * @param color the color name (case insensitive) or null to remove. + * Acceptable values are defined in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color setColor(String color) { + Color property = (color == null) ? null : new Color(color); + setColor(property); + return property; + } + + /** + * Gets the images that are associated with the calendar. + * @return the images (any changes made this list will affect the parent + * component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public List getImages() { + return getProperties(Image.class); + } + + /** + * Adds an image that is associated with the calendar. + * @param image the image + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public void addImage(Image image) { + addProperty(image); + } + + /** + * Gets the calendar's events. + * @return the events (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.52-5 + * @see RFC 2445 + * p.52-4 + * @see vCal 1.0 p.13 + */ + public List getEvents() { + return getComponents(VEvent.class); + } + + /** + * Adds an event to the calendar. + * @param event the event + * @see RFC 5545 + * p.52-5 + * @see RFC 2445 + * p.52-4 + * @see vCal 1.0 p.13 + */ + public void addEvent(VEvent event) { + addComponent(event); + } + + /** + * Gets the calendar's to-do tasks. + * @return the to-do tasks (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.55-7 + * @see RFC 2445 + * p.55-6 + * @see vCal 1.0 p.14 + */ + public List getTodos() { + return getComponents(VTodo.class); + } + + /** + * Adds a to-do task to the calendar. + * @param todo the to-do task + * @see RFC 5545 + * p.55-7 + * @see RFC 2445 + * p.55-6 + * @see vCal 1.0 p.14 + */ + public void addTodo(VTodo todo) { + addComponent(todo); + } + + /** + * Gets the calendar's journal entries. + * @return the journal entries (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.57-9 + * @see RFC 2445 + * p.56-7 + */ + public List getJournals() { + return getComponents(VJournal.class); + } + + /** + * Adds a journal entry to the calendar. + * @param journal the journal entry + * @see RFC 5545 + * p.57-9 + * @see RFC 2445 + * p.56-7 + */ + public void addJournal(VJournal journal) { + addComponent(journal); + } + + /** + * Gets the calendar's free/busy entries. + * @return the free/busy entries (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.59-62 + * @see RFC 2445 + * p.58-60 + */ + public List getFreeBusies() { + return getComponents(VFreeBusy.class); + } + + /** + * Adds a free/busy entry to the calendar. + * @param freeBusy the free/busy entry + * @see RFC 5545 + * p.59-62 + * @see RFC 2445 + * p.58-60 + */ + public void addFreeBusy(VFreeBusy freeBusy) { + addComponent(freeBusy); + } + + /** + *

+ * Checks this iCalendar object for data consistency problems or deviations + * from the specifications. + *

+ *

+ * The existence of validation warnings will not prevent the iCalendar + * object from being written to a data stream. Syntactically-correct output + * will still be produced. However, the consuming application may have + * trouble interpreting some of the data due to the presence of these + * warnings. + *

+ *

+ * These problems can largely be avoided by reading the Javadocs of the + * component and property classes, or by being familiar with the iCalendar + * standard. + *

+ * @param version the version to validate against + * @return the validation warnings + */ + public ValidationWarnings validate(ICalVersion version) { + List warnings = validate(new ArrayList(0), version); + return new ValidationWarnings(warnings); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version != ICalVersion.V1_0) { + checkRequiredCardinality(warnings, ProductId.class); + + if (this.components.isEmpty()) { + warnings.add(new ValidationWarning(4)); + } + + if (getProperty(Geo.class) != null) { + warnings.add(new ValidationWarning(44)); + } + } + + checkOptionalCardinality(warnings, Uid.class, LastModified.class, Url.class, RefreshInterval.class, Color.class, Source.class); + checkUniqueLanguages(warnings, Name.class); + checkUniqueLanguages(warnings, Description.class); + } + + private void checkUniqueLanguages(List warnings, Class clazz) { + List properties = getProperties(clazz); + if (properties.size() <= 1) { + return; + } + + Set languages = new HashSet(properties.size()); + for (ICalProperty property : properties) { + String language = property.getParameters().getLanguage(); + if (language != null) { + language = language.toLowerCase(); + } + + boolean added = languages.add(language); + if (!added) { + warnings.add(new ValidationWarning(55, clazz.getSimpleName())); + break; + } + } + } + + /** + *

+ * Marshals this iCalendar object to its traditional, plain-text + * representation. + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link ICalWriter} classes + * instead in order to register the scribe classes. + *

+ * @return the plain text representation + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + */ + public String write() { + ICalVersion version = (this.version == null) ? ICalVersion.V2_0 : this.version; + return Biweekly.write(this).version(version).go(); + } + + /** + *

+ * Marshals this iCalendar object to its traditional, plain-text + * representation. + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link ICalWriter} classes + * instead in order to register the scribe classes. + *

+ * @param file the file to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws IOException if there's an problem writing to the file + */ + public void write(File file) throws IOException { + ICalVersion version = (this.version == null) ? ICalVersion.V2_0 : this.version; + Biweekly.write(this).version(version).go(file); + } + + /** + *

+ * Marshals this iCalendar object to its traditional, plain-text + * representation. + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link ICalWriter} classes + * instead in order to register the scribe classes. + *

+ * @param out the output stream to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws IOException if there's a problem writing to the output stream + */ + public void write(OutputStream out) throws IOException { + ICalVersion version = (this.version == null) ? ICalVersion.V2_0 : this.version; + Biweekly.write(this).version(version).go(out); + } + + /** + *

+ * Marshals this iCalendar object to its traditional, plain-text + * representation. + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link ICalWriter} classes + * instead in order to register the scribe classes. + *

+ * @param writer the writer to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws IOException if there's a problem writing to the writer + */ + public void write(Writer writer) throws IOException { + ICalVersion version = (this.version == null) ? ICalVersion.V2_0 : this.version; + Biweekly.write(this).version(version).go(writer); + } + + /** + *

+ * Marshals this iCalendar object to its XML representation (xCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly}, {@link XCalWriter}, or + * {@link XCalDocument} classes instead in order to register the scribe + * classes. + *

+ * @return the XML document + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + */ + public String writeXml() { + return Biweekly.writeXml(this).indent(2).go(); + } + + /** + *

+ * Marshals this iCalendar object to its XML representation (xCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly}, {@link XCalWriter}, or + * {@link XCalDocument} classes instead in order to register the scribe + * classes. + *

+ * @param file the file to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws TransformerException if there's a problem writing to the file + * @throws IOException if there's a problem opening the file + */ + public void writeXml(File file) throws TransformerException, IOException { + Biweekly.writeXml(this).indent(2).go(file); + } + + /** + *

+ * Marshals this iCalendar object to its XML representation (xCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly}, {@link XCalWriter}, or + * {@link XCalDocument} classes instead in order to register the scribe + * classes. + *

+ * @param out the output stream to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws TransformerException if there's a problem writing to the output + * stream + */ + public void writeXml(OutputStream out) throws TransformerException { + Biweekly.writeXml(this).indent(2).go(out); + } + + /** + *

+ * Marshals this iCalendar object to its XML representation (xCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly}, {@link XCalWriter}, or + * {@link XCalDocument} classes instead in order to register the scribe + * classes. + *

+ * @param writer the writer to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws TransformerException if there's a problem writing to the writer + */ + public void writeXml(Writer writer) throws TransformerException { + Biweekly.writeXml(this).indent(2).go(writer); + } + + /** + *

+ * Marshals this iCalendar object to its JSON representation (jCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link JCalWriter} classes + * instead in order to register the scribe classes. + *

+ * @return the JSON string + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + */ + public String writeJson() { + return Biweekly.writeJson(this).go(); + } + + /** + *

+ * Marshals this iCalendar object to its JSON representation (jCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link JCalWriter} classes + * instead in order to register the scribe classes. + *

+ * @param file the file to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws IOException if there's a problem writing to the file + */ + public void writeJson(File file) throws IOException { + Biweekly.writeJson(this).go(file); + } + + /** + *

+ * Marshals this iCalendar object to its JSON representation (jCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link JCalWriter} classes + * instead in order to register the scribe classes. + *

+ * @param out the output stream to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws IOException if there's a problem writing to the output stream + */ + public void writeJson(OutputStream out) throws IOException { + Biweekly.writeJson(this).go(out); + } + + /** + *

+ * Marshals this iCalendar object to its JSON representation (jCal). + *

+ *

+ * If this iCalendar object contains user-defined property or component + * objects, you must use the {@link Biweekly} or {@link JCalWriter} classes + * instead in order to register the scribe classes. + *

+ * @param writer the writer to write to + * @throws IllegalArgumentException if this iCalendar object contains + * user-defined property or component objects + * @throws IOException if there's a problem writing to the writer + */ + public void writeJson(Writer writer) throws IOException { + Biweekly.writeJson(this).go(writer); + } + + @Override + protected Map toStringValues() { + Map fields = new HashMap(); + fields.put("version", version); + return fields; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (!super.equals(obj)) return false; + ICalendar other = (ICalendar) obj; + if (version != other.version) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/Messages.java b/app/src/main/java/biweekly/Messages.java new file mode 100644 index 0000000000..d129758ab2 --- /dev/null +++ b/app/src/main/java/biweekly/Messages.java @@ -0,0 +1,100 @@ +package biweekly; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Singleton for accessing the i18n resource bundle. + * @author Michael Angstadt + */ +public enum Messages { + INSTANCE; + + private final transient ResourceBundle messages; + + Messages() { + messages = ResourceBundle.getBundle("biweekly/messages"); + } + + /** + * Gets a validation warning message. + * @param code the message code + * @param args the message arguments + * @return the message + */ + public String getValidationWarning(int code, Object... args) { + return getMessage("validate." + code, args); + } + + /** + * Gets a parser warning message. + * @param code the message code + * @param args the message arguments + * @return the message + */ + public String getParseMessage(int code, Object... args) { + return getMessage("parse." + code, args); + } + + /** + * Gets an exception message. + * @param code the message code + * @param args the message arguments + * @return the message or null if not found + */ + public String getExceptionMessage(int code, Object... args) { + return getMessage("exception." + code, args); + } + + /** + * Builds an {@link IllegalArgumentException} from an exception message. + * @param code the message code + * @param args the message arguments + * @return the exception or null if the message was not found + */ + public IllegalArgumentException getIllegalArgumentException(int code, Object... args) { + String message = getExceptionMessage(code, args); + return (message == null) ? null : new IllegalArgumentException(message); + } + + /** + * Gets a message. + * @param key the message key + * @param args the message arguments + * @return the message or null if not found + */ + public String getMessage(String key, Object... args) { + try { + String message = messages.getString(key); + return MessageFormat.format(message, args); + } catch (MissingResourceException e) { + return null; + } + } +} diff --git a/app/src/main/java/biweekly/ValidationWarning.java b/app/src/main/java/biweekly/ValidationWarning.java new file mode 100644 index 0000000000..6ef1639740 --- /dev/null +++ b/app/src/main/java/biweekly/ValidationWarning.java @@ -0,0 +1,78 @@ +package biweekly; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a validation warning. + * @author Michael Angstadt + */ +public class ValidationWarning { + private final Integer code; + private final String message; + + /** + * Creates a new validation warning. + * @param code the warning message code + * @param args the warning message arguments + */ + public ValidationWarning(int code, Object... args) { + this.code = code; + this.message = Messages.INSTANCE.getValidationWarning(code, args); + } + + /** + * Creates a new validation warning. + * @param message the warning message + */ + public ValidationWarning(String message) { + this.code = null; + this.message = message; + } + + /** + * Gets the validation warning code. + * @return the warning code or null if no code was specified + */ + public Integer getCode() { + return code; + } + + /** + * Gets the validation warning message. + * @return the warning message + */ + public String getMessage() { + return message; + } + + @Override + public String toString() { + if (code == null) { + return message; + } + return "(" + code + ") " + message; + } +} diff --git a/app/src/main/java/biweekly/ValidationWarnings.java b/app/src/main/java/biweekly/ValidationWarnings.java new file mode 100644 index 0000000000..2048d97a4e --- /dev/null +++ b/app/src/main/java/biweekly/ValidationWarnings.java @@ -0,0 +1,289 @@ +package biweekly; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import biweekly.ValidationWarnings.WarningsGroup; +import biweekly.component.ICalComponent; +import biweekly.property.ICalProperty; +import biweekly.util.StringUtils; +import biweekly.util.StringUtils.JoinCallback; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Holds the validation warnings of an iCalendar object. + *

+ *

+ * Examples: + *

+ * + *
+ * //validate an iCalendar object
+ * ValidationWarnings warnings = ical.validate();
+ * 
+ * //print all warnings to a string:
+ * System.out.println(warnings.toString());
+ * //sample output:
+ * //[ICalendar]: ProductId is not set (it is a required property).
+ * //[ICalendar > VEvent > DateStart]: DateStart must come before DateEnd.
+ * //[ICalendar > VEvent > VAlarm]: The trigger must specify which date field its duration is relative to.
+ * 
+ * //iterate over each warnings group
+ * //this gives you access to the property/component object and its parent components
+ * for (WarningsGroup group : warnings) {
+ * ICalProperty prop = group.getProperty();
+ *   if (prop == null) {
+ *     //then it was a component that caused the warnings
+ *     ICalComponent comp = group.getComponent();
+ *   }
+ * 
+ *   //get parent components
+ *   List<ICalComponent> hierarchy = group.getComponentHierarchy();
+ * 
+ *   //get warning messages
+ *   List<String> messages = group.getMessages();
+ * }
+ * 
+ * //you can also get the warnings of specific properties/components
+ * List<WarningsGroup> dtstartWarnings = warnings.getByProperty(DateStart.class);
+ * List<WarningsGroup> veventWarnings = warnings.getByComponent(VEvent.class);
+ * 
+ * @author Michael Angstadt + * @see ICalendar#validate(ICalVersion) + */ +public class ValidationWarnings implements Iterable { + private final List warnings; + + /** + * Creates a new validation warnings list. + * @param warnings the validation warnings + */ + public ValidationWarnings(List warnings) { + this.warnings = warnings; + } + + /** + * Gets all validation warnings of a given property. + * @param propertyClass the property (e.g. {@code DateStart.class}) + * @return the validation warnings + */ + public List getByProperty(Class propertyClass) { + List warnings = new ArrayList(); + for (WarningsGroup group : this.warnings) { + ICalProperty property = group.getProperty(); + if (property == null) { + continue; + } + + if (propertyClass == property.getClass()) { + warnings.add(group); + } + } + return warnings; + } + + /** + * Gets all validation warnings of a given component. + * @param componentClass the component (e.g. {@code VEvent.class}) + * @return the validation warnings + */ + public List getByComponent(Class componentClass) { + List warnings = new ArrayList(); + for (WarningsGroup group : this.warnings) { + ICalComponent component = group.getComponent(); + if (component == null) { + continue; + } + + if (componentClass == component.getClass()) { + warnings.add(group); + } + } + return warnings; + } + + /** + * Gets all the validation warnings. + * @return the validation warnings + */ + public List getWarnings() { + return warnings; + } + + /** + * Determines whether there are any validation warnings. + * @return true if there are none, false if there are one or more + */ + public boolean isEmpty() { + return warnings.isEmpty(); + } + + /** + *

+ * Outputs all validation warnings as a newline-delimited string. For + * example: + *

+ * + *
+	 * [ICalendar]: ProductId is not set (it is a required property).
+	 * [ICalendar > VEvent > DateStart]: DateStart must come before DateEnd.
+	 * [ICalendar > VEvent > VAlarm]: The trigger must specify which date field its duration is relative to.
+	 * 
+ */ + @Override + public String toString() { + return StringUtils.join(warnings, StringUtils.NEWLINE); + } + + /** + * Iterates over each warning group (same as calling + * {@code getWarnings().iterator()}). + * @return the iterator + */ + public Iterator iterator() { + return warnings.iterator(); + } + + /** + * Holds the validation warnings of a property or component. + * @author Michael Angstadt + */ + public static class WarningsGroup { + private final ICalProperty property; + private final ICalComponent component; + private final List componentHierarchy; + private final List warnings; + + /** + * Creates a new set of validation warnings for a property. + * @param property the property that caused the warnings + * @param componentHierarchy the hierarchy of components that the + * property belongs to + * @param warning the warnings + */ + public WarningsGroup(ICalProperty property, List componentHierarchy, List warning) { + this(null, property, componentHierarchy, warning); + } + + /** + * Creates a new set of validation warnings for a component. + * @param component the component that caused the warnings + * @param componentHierarchy the hierarchy of components that the + * component belongs to + * @param warning the warnings + */ + public WarningsGroup(ICalComponent component, List componentHierarchy, List warning) { + this(component, null, componentHierarchy, warning); + } + + private WarningsGroup(ICalComponent component, ICalProperty property, List componentHierarchy, List warning) { + this.component = component; + this.property = property; + this.componentHierarchy = componentHierarchy; + this.warnings = warning; + } + + /** + * Gets the property object that caused the validation warnings. + * @return the property object or null if a component caused the + * warnings. + */ + public ICalProperty getProperty() { + return property; + } + + /** + * Gets the component object that caused the validation warnings. + * @return the component object or null if a property caused the + * warnings. + */ + public ICalComponent getComponent() { + return component; + } + + /** + * Gets the hierarchy of components that the property or component + * belongs to. + * @return the component hierarchy + */ + public List getComponentHierarchy() { + return componentHierarchy; + } + + /** + * Gets the warnings that belong to the property or component. + * @return the warnings + */ + public List getWarnings() { + return warnings; + } + + /** + *

+ * Outputs each message in this warnings group as a newline-delimited + * string. Each line includes the component hierarchy and the name of + * the property/component. For example: + *

+ * + *
+		 * [ICalendar > VEvent > VAlarm]: Email alarms must have at least one attendee.
+		 * [ICalendar > VEvent > VAlarm]: The trigger must specify which date field its duration is relative to.
+		 * 
+ */ + @Override + public String toString() { + final String prefix = "[" + buildPath() + "]: "; + return StringUtils.join(warnings, StringUtils.NEWLINE, new JoinCallback() { + public void handle(StringBuilder sb, ValidationWarning warning) { + sb.append(prefix).append(warning); + } + }); + } + + private String buildPath() { + StringBuilder sb = new StringBuilder(); + + if (!componentHierarchy.isEmpty()) { + String delimitor = " > "; + + StringUtils.join(componentHierarchy, delimitor, sb, new JoinCallback() { + public void handle(StringBuilder sb, ICalComponent component) { + sb.append(component.getClass().getSimpleName()); + } + }); + sb.append(delimitor); + } + + Object obj = (property == null) ? component : property; + sb.append(obj.getClass().getSimpleName()); + + return sb.toString(); + } + } +} diff --git a/app/src/main/java/biweekly/biweekly.license b/app/src/main/java/biweekly/biweekly.license new file mode 100644 index 0000000000..7df0740991 --- /dev/null +++ b/app/src/main/java/biweekly/biweekly.license @@ -0,0 +1,22 @@ + Copyright (c) 2013-2021, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/app/src/main/java/biweekly/biweekly.properties b/app/src/main/java/biweekly/biweekly.properties new file mode 100644 index 0000000000..ef804973d6 --- /dev/null +++ b/app/src/main/java/biweekly/biweekly.properties @@ -0,0 +1,4 @@ +version=0.6.8-SNAPSHOT +groupId=net.sf.biweekly +artifactId=biweekly +url=http://github.com/mangstadt/biweekly diff --git a/app/src/main/java/biweekly/commons-codec.license b/app/src/main/java/biweekly/commons-codec.license new file mode 100644 index 0000000000..75b52484ea --- /dev/null +++ b/app/src/main/java/biweekly/commons-codec.license @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/src/main/java/biweekly/component/DaylightSavingsTime.java b/app/src/main/java/biweekly/component/DaylightSavingsTime.java new file mode 100644 index 0000000000..787c3188a4 --- /dev/null +++ b/app/src/main/java/biweekly/component/DaylightSavingsTime.java @@ -0,0 +1,67 @@ +package biweekly.component; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the date range of a timezone's daylight savings time. + *

+ *

+ * Examples: + *

+ * + *
+ * VTimezone timezone = new VTimezone("Eastern Standard Time");
+ * DaylightSavingsTime daylight = new DaylightSavingsTime();
+ * DateTimeComponents components = new DateTimeComponents(1999, 4, 4, 2, 0, 0, false);
+ * daylight.setDateStart(components);
+ * daylight.setTimezoneOffsetFrom(-5, 0);
+ * daylight.setTimezoneOffsetTo(-4, 0);
+ * timezone.addDaylightSavingsTime(daylight);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 p.60-7 + */ +public class DaylightSavingsTime extends Observance { + public DaylightSavingsTime() { + //empty + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public DaylightSavingsTime(DaylightSavingsTime original) { + super(original); + } + + @Override + public DaylightSavingsTime copy() { + return new DaylightSavingsTime(this); + } +} diff --git a/app/src/main/java/biweekly/component/ICalComponent.java b/app/src/main/java/biweekly/component/ICalComponent.java new file mode 100644 index 0000000000..2081899635 --- /dev/null +++ b/app/src/main/java/biweekly/component/ICalComponent.java @@ -0,0 +1,833 @@ +package biweekly.component; + +import java.lang.reflect.Constructor; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.Messages; +import biweekly.ValidationWarnings.WarningsGroup; +import biweekly.ValidationWarning; +import biweekly.property.ICalProperty; +import biweekly.property.RawProperty; +import biweekly.property.Status; +import biweekly.util.ListMultimap; +import biweekly.util.StringUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Base class for all iCalendar component classes. + * @author Michael Angstadt + */ +public abstract class ICalComponent { + protected final ListMultimap, ICalComponent> components; + protected final ListMultimap, ICalProperty> properties; + + public ICalComponent() { + components = new ListMultimap, ICalComponent>(); + properties = new ListMultimap, ICalProperty>(); + } + + /** + * Copy constructor. Performs a deep copy of the given component's + * properties and sub-components. + * @param original the component to make a copy of + */ + protected ICalComponent(ICalComponent original) { + this(); + for (ICalProperty property : original.properties.values()) { + addProperty(property.copy()); + } + for (ICalComponent component : original.components.values()) { + addComponent(component.copy()); + } + } + + /** + * Gets the first property of a given class. + * @param clazz the property class + * @param the property class + * @return the property or null if not found + */ + public T getProperty(Class clazz) { + return clazz.cast(properties.first(clazz)); + } + + /** + * Gets all properties of a given class. Changes to the returned list will + * update the {@link ICalComponent} object, and vice versa. + * @param clazz the property class + * @param the property class + * @return the properties + */ + public List getProperties(Class clazz) { + return new ICalPropertyList(clazz); + } + + /** + * Gets all the properties associated with this component. + * @return the properties + */ + public ListMultimap, ICalProperty> getProperties() { + return properties; + } + + /** + * Adds a property to this component. + * @param property the property to add + */ + public void addProperty(ICalProperty property) { + properties.put(property.getClass(), property); + } + + /** + * Replaces all existing properties of the given property instance's class + * with the given property instance. + * @param property the property + * @return the replaced properties (this list is immutable) + */ + public List setProperty(ICalProperty property) { + return properties.replace(property.getClass(), property); + } + + /** + * Replaces all existing properties of the given class with a single + * property instance. If the property instance is null, then all instances + * of that property will be removed. + * @param clazz the property class (e.g. "DateStart.class") + * @param property the property or null to remove all properties of the + * given class + * @param the property class + * @return the replaced properties (this list is immutable) + */ + public List setProperty(Class clazz, T property) { + List replaced = properties.replace(clazz, property); + return castList(replaced, clazz); + } + + /** + * Removes a specific property instance from this component. + * @param property the property to remove + * @param the property class + * @return true if it was removed, false if it wasn't found + */ + public boolean removeProperty(T property) { + return properties.remove(property.getClass(), property); + } + + /** + * Removes all properties of a given class from this component. + * @param clazz the class of the properties to remove (e.g. + * "DateStart.class") + * @param the property class + * @return the removed properties (this list is immutable) + */ + public List removeProperties(Class clazz) { + List removed = properties.removeAll(clazz); + return castList(removed, clazz); + } + + /** + * Removes a specific sub-component instance from this component. + * @param component the component to remove + * @param the component class + * @return true if it was removed, false if it wasn't found + */ + public boolean removeComponent(T component) { + return components.remove(component.getClass(), component); + } + + /** + * Removes all sub-components of the given class from this component. + * @param clazz the class of the components to remove (e.g. "VEvent.class") + * @param the component class + * @return the removed components (this list is immutable) + */ + public List removeComponents(Class clazz) { + List removed = components.removeAll(clazz); + return castList(removed, clazz); + } + + /** + * Gets the first experimental property with a given name. + * @param name the property name (case insensitive, e.g. "X-ALT-DESC") + * @return the experimental property or null if none were found + */ + public RawProperty getExperimentalProperty(String name) { + for (RawProperty raw : getExperimentalProperties()) { + if (raw.getName().equalsIgnoreCase(name)) { + return raw; + } + } + return null; + } + + /** + * Gets all experimental properties with a given name. + * @param name the property name (case insensitive, e.g. "X-ALT-DESC") + * @return the experimental properties (this list is immutable) + */ + public List getExperimentalProperties(String name) { + /* + * Note: The returned list is not backed by the parent component because + * this would allow RawProperty objects without the specified name to be + * added to the list. + */ + List toReturn = new ArrayList(); + for (RawProperty property : getExperimentalProperties()) { + if (property.getName().equalsIgnoreCase(name)) { + toReturn.add(property); + } + } + return Collections.unmodifiableList(toReturn); + } + + /** + * Gets all experimental properties associated with this component. Changes + * to the returned list will update the {@link ICalComponent} object, and + * vice versa. + * @return the experimental properties + */ + public List getExperimentalProperties() { + return getProperties(RawProperty.class); + } + + /** + * Adds an experimental property to this component. + * @param name the property name (e.g. "X-ALT-DESC") + * @param value the property value + * @return the property object that was created + */ + public RawProperty addExperimentalProperty(String name, String value) { + return addExperimentalProperty(name, null, value); + } + + /** + * Adds an experimental property to this component. + * @param name the property name (e.g. "X-ALT-DESC") + * @param dataType the property's data type or null if unknown + * @param value the property value + * @return the property object that was created + */ + public RawProperty addExperimentalProperty(String name, ICalDataType dataType, String value) { + RawProperty raw = new RawProperty(name, dataType, value); + addProperty(raw); + return raw; + } + + /** + * Adds an experimental property to this component, removing all existing + * properties that have the same name. + * @param name the property name (e.g. "X-ALT-DESC") + * @param value the property value + * @return the property object that was created + */ + public RawProperty setExperimentalProperty(String name, String value) { + return setExperimentalProperty(name, null, value); + } + + /** + * Adds an experimental property to this component, removing all existing + * properties that have the same name. + * @param name the property name (e.g. "X-ALT-DESC") + * @param dataType the property's data type or null if unknown + * @param value the property value + * @return the property object that was created + */ + public RawProperty setExperimentalProperty(String name, ICalDataType dataType, String value) { + removeExperimentalProperties(name); + return addExperimentalProperty(name, dataType, value); + } + + /** + * Removes all experimental properties that have the given name. + * @param name the component name (e.g. "X-ALT-DESC") + * @return the removed properties (this list is immutable) + */ + public List removeExperimentalProperties(String name) { + List all = getExperimentalProperties(); + List toRemove = new ArrayList(); + for (RawProperty property : all) { + if (property.getName().equalsIgnoreCase(name)) { + toRemove.add(property); + } + } + + all.removeAll(toRemove); + return Collections.unmodifiableList(toRemove); + } + + /** + * Gets the first sub-component of a given class. + * @param clazz the component class + * @param the component class + * @return the sub-component or null if not found + */ + public T getComponent(Class clazz) { + return clazz.cast(components.first(clazz)); + } + + /** + * Gets all sub-components of a given class. Changes to the returned list + * will update the parent component object, and vice versa. + * @param clazz the component class + * @param the component class + * @return the sub-components + */ + public List getComponents(Class clazz) { + return new ICalComponentList(clazz); + } + + /** + * Gets all the sub-components associated with this component. + * @return the sub-components + */ + public ListMultimap, ICalComponent> getComponents() { + return components; + } + + /** + * Adds a sub-component to this component. + * @param component the component to add + */ + public void addComponent(ICalComponent component) { + components.put(component.getClass(), component); + } + + /** + * Replaces all sub-components of a given class with the given component. + * @param component the component + * @return the replaced sub-components (this list is immutable) + */ + public List setComponent(ICalComponent component) { + return components.replace(component.getClass(), component); + } + + /** + * Replaces all sub-components of a given class with the given component. If + * the component instance is null, then all instances of that component will + * be removed. + * @param clazz the component's class + * @param component the component or null to remove all components of the + * given class + * @param the component class + * @return the replaced sub-components (this list is immutable) + */ + public List setComponent(Class clazz, T component) { + List replaced = components.replace(clazz, component); + return castList(replaced, clazz); + } + + /** + * Gets the first experimental sub-component with a given name. + * @param name the component name (case insensitive, e.g. "X-PARTY") + * @return the experimental component or null if none were found + */ + public RawComponent getExperimentalComponent(String name) { + for (RawComponent component : getExperimentalComponents()) { + if (component.getName().equalsIgnoreCase(name)) { + return component; + } + } + return null; + } + + /** + * Gets all experimental sub-component with a given name. + * @param name the component name (case insensitive, e.g. "X-PARTY") + * @return the experimental components (this list is immutable) + */ + public List getExperimentalComponents(String name) { + /* + * Note: The returned list is not backed by the parent component because + * this would allow RawComponent objects without the specified name to + * be added to the list. + */ + List toReturn = new ArrayList(); + for (RawComponent component : getExperimentalComponents()) { + if (component.getName().equalsIgnoreCase(name)) { + toReturn.add(component); + } + } + return Collections.unmodifiableList(toReturn); + } + + /** + * Gets all experimental sub-components associated with this component. + * Changes to the returned list will update the parent {@link ICalComponent} + * object, and vice versa. + * @return the experimental components + */ + public List getExperimentalComponents() { + return getComponents(RawComponent.class); + } + + /** + * Adds an experimental sub-component to this component. + * @param name the component name (e.g. "X-PARTY") + * @return the component object that was created + */ + public RawComponent addExperimentalComponent(String name) { + RawComponent raw = new RawComponent(name); + addComponent(raw); + return raw; + } + + /** + * Adds an experimental sub-component to this component, removing all + * existing components that have the same name. + * @param name the component name (e.g. "X-PARTY") + * @return the component object that was created + */ + public RawComponent setExperimentalComponent(String name) { + removeExperimentalComponents(name); + return addExperimentalComponent(name); + } + + /** + * Removes all experimental sub-components that have the given name. + * @param name the component name (e.g. "X-PARTY") + * @return the removed sub-components (this list is immutable) + */ + public List removeExperimentalComponents(String name) { + List all = getExperimentalComponents(); + List toRemove = new ArrayList(); + for (RawComponent property : all) { + if (property.getName().equalsIgnoreCase(name)) { + toRemove.add(property); + } + } + + all.removeAll(toRemove); + return Collections.unmodifiableList(toRemove); + } + + /** + *

+ * Checks this component for data consistency problems or deviations from + * the specifications. + *

+ *

+ * The existence of validation warnings will not prevent the component + * object from being written to a data stream. Syntactically-correct output + * will still be produced. However, the consuming application may have + * trouble interpreting some of the data due to the presence of these + * warnings. + *

+ *

+ * These problems can largely be avoided by reading the Javadocs of the + * component and property classes, or by being familiar with the iCalendar + * standard. + *

+ * @param hierarchy the hierarchy of components that the component belongs + * to + * @param version the version to validate against + * @see ICalendar#validate(List, ICalVersion) + * @return a list of warnings or an empty list if no problems were found + */ + public final List validate(List hierarchy, ICalVersion version) { + List warnings = new ArrayList(); + + //validate this component + List warningsBuf = new ArrayList(0); + validate(hierarchy, version, warningsBuf); + if (!warningsBuf.isEmpty()) { + warnings.add(new WarningsGroup(this, hierarchy, warningsBuf)); + } + + //add this component to the hierarchy list + //copy the list so other validate() calls aren't effected + hierarchy = new ArrayList(hierarchy); + hierarchy.add(this); + + //validate properties + for (ICalProperty property : properties.values()) { + List propWarnings = property.validate(hierarchy, version); + if (!propWarnings.isEmpty()) { + warnings.add(new WarningsGroup(property, hierarchy, propWarnings)); + } + } + + //validate sub-components + for (ICalComponent component : components.values()) { + warnings.addAll(component.validate(hierarchy, version)); + } + + return warnings; + } + + /** + *

+ * Checks the component for data consistency problems or deviations from the + * spec. + *

+ *

+ * This method should be overridden by child classes that wish to provide + * validation logic. The default implementation of this method does nothing. + *

+ * @param components the hierarchy of components that the component belongs + * to + * @param version the version to validate against + * @param warnings the list to add the warnings to + */ + protected void validate(List components, ICalVersion version, List warnings) { + //do nothing + } + + /** + * Utility method for validating that there is exactly one instance of each + * of the given properties. + * @param warnings the list to add the warnings to + * @param classes the properties to check + */ + protected void checkRequiredCardinality(List warnings, Class... classes) { + for (Class clazz : classes) { + List props = getProperties(clazz); + + if (props.isEmpty()) { + warnings.add(new ValidationWarning(2, clazz.getSimpleName())); + continue; + } + + if (props.size() > 1) { + warnings.add(new ValidationWarning(3, clazz.getSimpleName())); + continue; + } + } + } + + /** + * Utility method for validating that there is no more than one instance of + * each of the given properties. + * @param warnings the list to add the warnings to + * @param classes the properties to check + */ + protected void checkOptionalCardinality(List warnings, Class... classes) { + for (Class clazz : classes) { + List props = getProperties(clazz); + + if (props.size() > 1) { + warnings.add(new ValidationWarning(3, clazz.getSimpleName())); + continue; + } + } + } + + /** + * Utility method for validating the {@link Status} property of a component. + * @param warnings the list to add the warnings to + * @param allowed the valid statuses + */ + protected void checkStatus(List warnings, Status... allowed) { + Status actual = getProperty(Status.class); + if (actual == null) { + return; + } + + List allowedValues = new ArrayList(allowed.length); + for (Status status : allowed) { + String value = status.getValue().toLowerCase(); + allowedValues.add(value); + } + + String actualValue = actual.getValue().toLowerCase(); + if (!allowedValues.contains(actualValue)) { + warnings.add(new ValidationWarning(13, actual.getValue(), allowedValues)); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(0, sb); + return sb.toString(); + } + + /** + *

+ * Gets string representations of any additional fields the component has + * (other than sub-components and properties) for the {@link #toString} + * method. + *

+ *

+ * Meant to be overridden by child classes. The default implementation + * returns an empty map. + *

+ * @return the values of the component's fields (key = field name, value = + * field value) + */ + protected Map toStringValues() { + return Collections.emptyMap(); + } + + private void toString(int depth, StringBuilder sb) { + StringUtils.repeat(' ', depth * 2, sb); + sb.append(getClass().getName()); + + Map fields = toStringValues(); + if (!fields.isEmpty()) { + sb.append(' ').append(fields.toString()); + } + + sb.append(StringUtils.NEWLINE); + + depth++; + for (ICalProperty property : properties.values()) { + StringUtils.repeat(' ', depth * 2, sb); + sb.append(property).append(StringUtils.NEWLINE); + } + for (ICalComponent component : components.values()) { + component.toString(depth, sb); + } + } + + /** + *

+ * Creates a deep copy of this component object. + *

+ *

+ * The default implementation of this method uses reflection to look for a + * copy constructor. Child classes SHOULD override this method to avoid the + * performance overhead involved in using reflection. + *

+ *

+ * The child class's copy constructor, if present, MUST invoke the + * {@link #ICalComponent(ICalComponent)} super constructor to ensure that + * the component's properties and sub-components are copied. + *

+ *

+ * This method MUST be overridden by the child class if the child class does + * not have a copy constructor. Otherwise, an + * {@link UnsupportedOperationException} will be thrown when an attempt is + * made to copy the component (such as in the + * {@link ICalendar#ICalendar(ICalendar) ICalendar class's copy constructor} + * ). + *

+ * @return the copy + * @throws UnsupportedOperationException if the class does not have a copy + * constructor or there is a problem invoking it + */ + public ICalComponent copy() { + Class clazz = getClass(); + + try { + Constructor copyConstructor = clazz.getConstructor(clazz); + return copyConstructor.newInstance(this); + } catch (Exception e) { + throw new UnsupportedOperationException(Messages.INSTANCE.getExceptionMessage(1, clazz.getName()), e); + } + } + + /** + * Casts all objects in the given list to the given class, adding the casted + * objects to a new list. + * @param list the list to cast + * @param castTo the class to cast to + * @param the class to cast to + * @return the new list (immutable) + */ + private static List castList(List list, Class castTo) { + List casted = new ArrayList(list.size()); + for (Object object : list) { + casted.add(castTo.cast(object)); + } + return Collections.unmodifiableList(casted); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + + int propertiesHash = 1; + for (ICalProperty property : properties.values()) { + propertiesHash += property.hashCode(); + } + result = prime * result + propertiesHash; + + int componentsHash = 1; + for (ICalComponent component : components.values()) { + componentsHash += component.hashCode(); + } + result = prime * result + componentsHash; + + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ICalComponent other = (ICalComponent) obj; + + if (properties.size() != other.properties.size()) return false; + if (components.size() != other.components.size()) return false; + + if (!compareMultimaps(properties, other.properties)) return false; + if (!compareMultimaps(components, other.components)) return false; + + return true; + } + + private static boolean compareMultimaps(ListMultimap map1, ListMultimap map2) { + for (Map.Entry> entry : map1) { + K key = entry.getKey(); + List value = entry.getValue(); + List otherValue = map2.get(key); + + if (value.size() != otherValue.size()) { + return false; + } + + List otherValueCopy = new ArrayList(otherValue); + for (V property : value) { + if (!otherValueCopy.remove(property)) { + return false; + } + } + } + return true; + } + + /** + *

+ * A list that automatically casts {@link ICalComponent} instances stored in + * this component to a given component class. + *

+ *

+ * This list is backed by the {@link ICalComponent} object. Any changes made + * to the list will affect the {@link ICalComponent} object and vice versa. + *

+ * @param the component class + */ + private class ICalComponentList extends AbstractList { + protected final Class componentClass; + protected final List components; + + /** + * @param componentClass the component class + */ + public ICalComponentList(Class componentClass) { + this.componentClass = componentClass; + components = ICalComponent.this.components.get(componentClass); + } + + @Override + public void add(int index, T value) { + components.add(index, value); + } + + @Override + public T remove(int index) { + ICalComponent removed = components.remove(index); + return cast(removed); + } + + @Override + public T get(int index) { + ICalComponent property = components.get(index); + return cast(property); + } + + @Override + public T set(int index, T value) { + ICalComponent replaced = components.set(index, value); + return cast(replaced); + } + + @Override + public int size() { + return components.size(); + } + + protected T cast(ICalComponent value) { + return componentClass.cast(value); + } + } + + /** + *

+ * A list that automatically casts {@link ICalProperty} instances stored in + * this component to a given property class. + *

+ *

+ * This list is backed by the {@link ICalComponent} object. Any changes made + * to the list will affect the {@link ICalComponent} object and vice versa. + *

+ * @param the property class + */ + private class ICalPropertyList extends AbstractList { + protected final Class propertyClass; + protected final List properties; + + /** + * @param propertyClass the property class + */ + public ICalPropertyList(Class propertyClass) { + this.propertyClass = propertyClass; + properties = ICalComponent.this.properties.get(propertyClass); + } + + @Override + public void add(int index, T value) { + properties.add(index, value); + } + + @Override + public T remove(int index) { + ICalProperty removed = properties.remove(index); + return cast(removed); + } + + @Override + public T get(int index) { + ICalProperty property = properties.get(index); + return cast(property); + } + + @Override + public T set(int index, T value) { + ICalProperty replaced = properties.set(index, value); + return cast(replaced); + } + + @Override + public int size() { + return properties.size(); + } + + protected T cast(ICalProperty value) { + return propertyClass.cast(value); + } + } +} diff --git a/app/src/main/java/biweekly/component/Observance.java b/app/src/main/java/biweekly/component/Observance.java new file mode 100644 index 0000000000..838983ebad --- /dev/null +++ b/app/src/main/java/biweekly/component/Observance.java @@ -0,0 +1,409 @@ +package biweekly.component; + +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.property.Comment; +import biweekly.property.DateStart; +import biweekly.property.ExceptionDates; +import biweekly.property.RecurrenceDates; +import biweekly.property.RecurrenceRule; +import biweekly.property.TimezoneName; +import biweekly.property.TimezoneOffsetFrom; +import biweekly.property.TimezoneOffsetTo; +import biweekly.util.DateTimeComponents; +import biweekly.util.ICalDate; +import biweekly.util.Recurrence; +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a timezone observance (i.e. "daylight savings" and "standard" + * times). + * @author Michael Angstadt + * @see DaylightSavingsTime + * @see StandardTime + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 p.60-7 + */ +/* + * Note: References to the vCal 1.0 spec are omitted from the property + * getter/setter method Javadocs because vCal does not use the VTIMEZONE + * component. + */ +public class Observance extends ICalComponent { + public Observance() { + //empty + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public Observance(Observance original) { + super(original); + } + + /** + * Gets the date that the timezone observance starts. + * @return the start date or null if not set + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart getDateStart() { + return getProperty(DateStart.class); + } + + /** + * Sets the date that the timezone observance starts. + * @param dateStart the start date or null to remove + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public void setDateStart(DateStart dateStart) { + setProperty(DateStart.class, dateStart); + } + + /** + * Sets the date that the timezone observance starts. + * @param date the start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart setDateStart(ICalDate date) { + DateStart prop = (date == null) ? null : new DateStart(date); + setDateStart(prop); + return prop; + } + + /** + * Sets the date that the timezone observance starts. + * @param rawComponents the start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart setDateStart(DateTimeComponents rawComponents) { + return setDateStart((rawComponents == null) ? null : new ICalDate(rawComponents, true)); + } + + /** + * Gets the UTC offset that the timezone observance transitions to. + * @return the UTC offset or null if not set + * @see RFC 5545 + * p.105-6 + * @see RFC 2445 + * p.100-1 + */ + public TimezoneOffsetTo getTimezoneOffsetTo() { + return getProperty(TimezoneOffsetTo.class); + } + + /** + * Sets the UTC offset that the timezone observance transitions to. + * @param timezoneOffsetTo the UTC offset or null to remove + * @see RFC 5545 + * p.105-6 + * @see RFC 2445 + * p.100-1 + */ + public void setTimezoneOffsetTo(TimezoneOffsetTo timezoneOffsetTo) { + setProperty(TimezoneOffsetTo.class, timezoneOffsetTo); + } + + /** + * Sets the UTC offset that the timezone observance transitions to. + * @param offset the offset + * @return the property that was created + * @see RFC 5545 + * p.105-6 + * @see RFC 2445 + * p.100-1 + */ + public TimezoneOffsetTo setTimezoneOffsetTo(UtcOffset offset) { + TimezoneOffsetTo prop = new TimezoneOffsetTo(offset); + setTimezoneOffsetTo(prop); + return prop; + } + + /** + * Gets the UTC offset that the timezone observance transitions from. + * @return the UTC offset or null if not set + * @see RFC 5545 + * p.104-5 + * @see RFC 2445 + * p.99-100 + */ + public TimezoneOffsetFrom getTimezoneOffsetFrom() { + return getProperty(TimezoneOffsetFrom.class); + } + + /** + * Sets the UTC offset that the timezone observance transitions from. + * @param timezoneOffsetFrom the UTC offset or null to remove + * @see RFC 5545 + * p.104-5 + * @see RFC 2445 + * p.99-100 + */ + public void setTimezoneOffsetFrom(TimezoneOffsetFrom timezoneOffsetFrom) { + setProperty(TimezoneOffsetFrom.class, timezoneOffsetFrom); + } + + /** + * Sets the UTC offset that the timezone observance transitions from. + * @param offset the offset + * @return the property that was created + * @see RFC 5545 + * p.104-5 + * @see RFC 2445 + * p.99-100 + */ + public TimezoneOffsetFrom setTimezoneOffsetFrom(UtcOffset offset) { + TimezoneOffsetFrom prop = new TimezoneOffsetFrom(offset); + setTimezoneOffsetFrom(prop); + return prop; + } + + /** + * Gets how often the timezone observance repeats. + * @return the recurrence rule or null if not set + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + */ + public RecurrenceRule getRecurrenceRule() { + return getProperty(RecurrenceRule.class); + } + + /** + * Sets how often the timezone observance repeats. + * @param recur the recurrence rule or null to remove + * @return the property that was created + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + */ + public RecurrenceRule setRecurrenceRule(Recurrence recur) { + RecurrenceRule prop = (recur == null) ? null : new RecurrenceRule(recur); + setRecurrenceRule(prop); + return prop; + } + + /** + * Sets how often the timezone observance repeats. + * @param recurrenceRule the recurrence rule or null to remove + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + */ + public void setRecurrenceRule(RecurrenceRule recurrenceRule) { + setProperty(RecurrenceRule.class, recurrenceRule); + } + + /** + * Gets the comments attached to the timezone observance. + * @return the comments (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public List getComments() { + return getProperties(Comment.class); + } + + /** + * Adds a comment to the timezone observance. + * @param comment the comment to add + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public void addComment(Comment comment) { + addProperty(comment); + } + + /** + * Adds a comment to the timezone observance. + * @param comment the comment to add + * @return the property that was created + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public Comment addComment(String comment) { + Comment prop = new Comment(comment); + addComment(prop); + return prop; + } + + /** + * Gets the list of dates/periods that help define the recurrence rule of + * this timezone observance (if one is defined). + * @return the recurrence dates (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + */ + public List getRecurrenceDates() { + return getProperties(RecurrenceDates.class); + } + + /** + * Adds a list of dates/periods that help define the recurrence rule of this + * timezone observance (if one is defined). + * @param recurrenceDates the recurrence dates + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + */ + public void addRecurrenceDates(RecurrenceDates recurrenceDates) { + addProperty(recurrenceDates); + } + + /** + * Gets the traditional, non-standard names for the timezone observance. + * @return the timezone observance names (any changes made this list will + * affect the parent component object and vice versa) + * @see RFC 5545 + * p.103-4 + * @see RFC 2445 + * p.98-9 + */ + public List getTimezoneNames() { + return getProperties(TimezoneName.class); + } + + /** + * Adds a traditional, non-standard name for the timezone observance. + * @param timezoneName the timezone observance name + * @see RFC 5545 + * p.103-4 + * @see RFC 2445 + * p.98-9 + */ + public void addTimezoneName(TimezoneName timezoneName) { + addProperty(timezoneName); + } + + /** + * Adds a traditional, non-standard name for the timezone observance. + * @param timezoneName the timezone observance name (e.g. "EST") + * @return the property that was created + * @see RFC 5545 + * p.103-4 + * @see RFC 2445 + * p.98-9 + */ + public TimezoneName addTimezoneName(String timezoneName) { + TimezoneName prop = new TimezoneName(timezoneName); + addTimezoneName(prop); + return prop; + } + + /** + * Gets the list of exceptions to the timezone observance. + * @return the list of exceptions (any changes made this list will affect + * the parent component object and vice versa) + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + */ + public List getExceptionDates() { + return getProperties(ExceptionDates.class); + } + + /** + * Adds a list of exceptions to the timezone observance. Note that this + * property can contain multiple dates. + * @param exceptionDates the list of exceptions + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + */ + public void addExceptionDates(ExceptionDates exceptionDates) { + addProperty(exceptionDates); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version == ICalVersion.V1_0) { + warnings.add(new ValidationWarning(48, version)); + } + + checkRequiredCardinality(warnings, DateStart.class, TimezoneOffsetTo.class, TimezoneOffsetFrom.class); + + //BYHOUR, BYMINUTE, and BYSECOND cannot be specified in RRULE if DTSTART's data type is "date" + //RFC 5545 p. 167 + DateStart dateStart = getDateStart(); + RecurrenceRule rrule = getRecurrenceRule(); + if (dateStart != null && rrule != null) { + ICalDate start = dateStart.getValue(); + Recurrence recur = rrule.getValue(); + if (start != null && recur != null) { + if (!start.hasTime() && (!recur.getByHour().isEmpty() || !recur.getByMinute().isEmpty() || !recur.getBySecond().isEmpty())) { + warnings.add(new ValidationWarning(5)); + } + } + } + + //there *should* be only 1 instance of RRULE + //RFC 5545 p. 167 + if (getProperties(RecurrenceRule.class).size() > 1) { + warnings.add(new ValidationWarning(6)); + } + } + + @Override + public Observance copy() { + return new Observance(this); + } +} diff --git a/app/src/main/java/biweekly/component/RawComponent.java b/app/src/main/java/biweekly/component/RawComponent.java new file mode 100644 index 0000000000..0ce9db5f3e --- /dev/null +++ b/app/src/main/java/biweekly/component/RawComponent.java @@ -0,0 +1,74 @@ +package biweekly.component; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a component that does not have a scribe associated with it. + * @author Michael Angstadt + */ +public class RawComponent extends ICalComponent { + private final String name; + + public RawComponent(String name) { + this.name = name; + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public RawComponent(RawComponent original) { + super(original); + this.name = original.name; + } + + public String getName() { + return name; + } + + @Override + public RawComponent copy() { + return new RawComponent(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (!super.equals(obj)) return false; + RawComponent other = (RawComponent) obj; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/component/StandardTime.java b/app/src/main/java/biweekly/component/StandardTime.java new file mode 100644 index 0000000000..24d46a334a --- /dev/null +++ b/app/src/main/java/biweekly/component/StandardTime.java @@ -0,0 +1,67 @@ +package biweekly.component; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the date range of a timezone's standard time. + *

+ *

+ * Examples: + *

+ * + *
+ * VTimezone timezone = new VTimezone("Eastern Standard Time");
+ * StandardTime standard = new StandardTime();
+ * DateTimeComponents components = new DateTimeComponents(1998, 10, 25, 2, 0, 0, false);
+ * standard.setDateStart(components);
+ * standard.setTimezoneOffsetFrom(-4, 0);
+ * standard.setTimezoneOffsetTo(-5, 0);
+ * timezone.addStandardTime(standard);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 p.60-7 + */ +public class StandardTime extends Observance { + public StandardTime() { + //empty + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public StandardTime(StandardTime original) { + super(original); + } + + @Override + public StandardTime copy() { + return new StandardTime(this); + } +} diff --git a/app/src/main/java/biweekly/component/VAlarm.java b/app/src/main/java/biweekly/component/VAlarm.java new file mode 100644 index 0000000000..207cd557b5 --- /dev/null +++ b/app/src/main/java/biweekly/component/VAlarm.java @@ -0,0 +1,590 @@ +package biweekly.component; + +import java.util.Arrays; +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.parameter.Related; +import biweekly.property.Action; +import biweekly.property.Attachment; +import biweekly.property.Attendee; +import biweekly.property.DateDue; +import biweekly.property.DateEnd; +import biweekly.property.DateStart; +import biweekly.property.Description; +import biweekly.property.DurationProperty; +import biweekly.property.Repeat; +import biweekly.property.Summary; +import biweekly.property.Trigger; +import biweekly.util.Duration; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a reminder for an event or to-do task. This class contains static + * factory methods to aid in the construction of valid alarms. + *

+ *

+ * Examples: + *

+ * + *
+ * //audio alarm
+ * Trigger trigger = ...
+ * Attachment sound = ...
+ * VAlarm audio = VAlarm.audio(trigger, sound);
+ * 
+ * //display alarm
+ * Trigger trigger = ...
+ * String message = "Meeting at 1pm";
+ * VAlarm display = VAlarm.display(trigger, message);
+ * 
+ * //email alarm
+ * Trigger trigger = ...
+ * String subject = "Reminder: Meeting at 1pm";
+ * String body = "Team,\n\nThe team meeting scheduled for 1pm is about to start.  Snacks will be served!\n\nThanks,\nJohn";
+ * List<String> to = Arrays.asList("janedoe@example.com", "bobsmith@example.com");
+ * VAlarm email = VAlarm.email(trigger, subject, body, to);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.71-6 + * @see RFC 2445 + * p.67-73 + */ +/* + * Note: References to the vCal 1.0 spec are omitted from the property + * getter/setter method Javadocs because vCal does not use the VALARM component. + */ +public class VAlarm extends ICalComponent { + /** + * Creates a new alarm. Consider using one of the static factory methods + * instead. + * @param action the alarm action (e.g. "email") + * @param trigger the trigger + */ + public VAlarm(Action action, Trigger trigger) { + setAction(action); + setTrigger(trigger); + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public VAlarm(VAlarm original) { + super(original); + } + + /** + * Creates an audio alarm. + * @param trigger the trigger + * @return the alarm + */ + public static VAlarm audio(Trigger trigger) { + return audio(trigger, null); + } + + /** + * Creates an audio alarm. + * @param trigger the trigger + * @param sound a sound to play when the alarm triggers + * @return the alarm + */ + public static VAlarm audio(Trigger trigger, Attachment sound) { + VAlarm alarm = new VAlarm(Action.audio(), trigger); + if (sound != null) { + alarm.addAttachment(sound); + } + return alarm; + } + + /** + * Creates a display alarm. + * @param trigger the trigger + * @param displayText the display text + * @return the alarm + */ + public static VAlarm display(Trigger trigger, String displayText) { + VAlarm alarm = new VAlarm(Action.display(), trigger); + alarm.setDescription(displayText); + return alarm; + } + + /** + * Creates an email alarm. + * @param trigger the trigger + * @param subject the email subject + * @param body the email body + * @param recipients the email address(es) to send the alert to + * @return the alarm + */ + public static VAlarm email(Trigger trigger, String subject, String body, String... recipients) { + return email(trigger, subject, body, Arrays.asList(recipients)); + } + + /** + * Creates a procedure alarm (vCal 1.0 only). + * @param trigger the trigger + * @param path the path or name of the procedure + * @return the alarm + */ + public static VAlarm procedure(Trigger trigger, String path) { + VAlarm alarm = new VAlarm(Action.procedure(), trigger); + alarm.setDescription(path); + return alarm; + } + + /** + * Creates an email alarm. + * @param trigger the trigger + * @param subject the email subject + * @param body the email body + * @param recipients the email address(es) to send the alert to + * @return the alarm + */ + public static VAlarm email(Trigger trigger, String subject, String body, List recipients) { + VAlarm alarm = new VAlarm(Action.email(), trigger); + alarm.setSummary(subject); + alarm.setDescription(body); + for (String recipient : recipients) { + alarm.addAttendee(new Attendee(null, recipient)); + } + return alarm; + } + + /** + * Gets any attachments that are associated with the alarm. + * @return the attachments (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + */ + public List getAttachments() { + return getProperties(Attachment.class); + } + + /** + * Adds an attachment to the alarm. Note that AUDIO alarms should only have + * 1 attachment. + * @param attachment the attachment to add + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + */ + public void addAttachment(Attachment attachment) { + addProperty(attachment); + } + + /** + *

+ * Gets a detailed description of the alarm. The description should be more + * detailed than the one provided by the {@link Summary} property. + *

+ *

+ * This property has different meanings, depending on the alarm action: + *

+ *
    + *
  • DISPLAY - the display text
  • + *
  • EMAIL - the body of the email message
  • + *
  • all others - a general description of the alarm
  • + *
+ * @return the description or null if not set + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + */ + public Description getDescription() { + return getProperty(Description.class); + } + + /** + *

+ * Sets a detailed description of the alarm. The description should be more + * detailed than the one provided by the {@link Summary} property. + *

+ *

+ * This property has different meanings, depending on the alarm action: + *

+ *
    + *
  • DISPLAY - the display text
  • + *
  • EMAIL - the body of the email message
  • + *
  • all others - a general description of the alarm
  • + *
+ * @param description the description or null to remove + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + */ + public void setDescription(Description description) { + setProperty(Description.class, description); + } + + /** + *

+ * Sets a detailed description of the alarm. The description should be more + * detailed than the one provided by the {@link Summary} property. + *

+ *

+ * This property has different meanings, depending on the alarm action: + *

+ *
    + *
  • DISPLAY - the display text
  • + *
  • EMAIL - the body of the email message
  • + *
  • all others - a general description of the alarm
  • + *
+ * @param description the description or null to remove + * @return the property that was created + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + */ + public Description setDescription(String description) { + Description prop = (description == null) ? null : new Description(description); + setDescription(prop); + return prop; + } + + /** + *

+ * Gets the summary of the alarm. + *

+ *

+ * This property has different meanings, depending on the alarm action: + *

+ *
    + *
  • EMAIL - the subject line of the email
  • + *
  • all others - a one-line summary of the alarm
  • + *
+ * @return the summary or null if not set + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + */ + public Summary getSummary() { + return getProperty(Summary.class); + } + + /** + *

+ * Sets the summary of the alarm. + *

+ *

+ * This property has different meanings, depending on the alarm action: + *

+ *
    + *
  • EMAIL - the subject line of the email
  • + *
  • all others - a one-line summary of the alarm
  • + *
+ * @param summary the summary or null to remove + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + */ + public void setSummary(Summary summary) { + setProperty(Summary.class, summary); + } + + /** + *

+ * Sets the summary of the alarm. + *

+ *

+ * This property has different meanings, depending on the alarm action: + *

+ *
    + *
  • EMAIL - the subject line of the email
  • + *
  • all others - a one-line summary of the alarm
  • + *
+ * @param summary the summary or null to remove + * @return the property that was created + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + */ + public Summary setSummary(String summary) { + Summary prop = (summary == null) ? null : new Summary(summary); + setSummary(prop); + return prop; + } + + /** + * Gets the people who will be emailed when the alarm fires (only applicable + * for EMAIL alarms). + * @return the email recipients (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public List getAttendees() { + return getProperties(Attendee.class); + } + + /** + * Adds a person who will be emailed when the alarm fires (only applicable + * for EMAIL alarms). + * @param attendee the email recipient + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public void addAttendee(Attendee attendee) { + addProperty(attendee); + } + + /** + * Gets the type of action to invoke when the alarm is triggered. + * @return the action or null if not set + * @see RFC 5545 + * p.132-3 + * @see RFC 2445 + * p.126 + */ + public Action getAction() { + return getProperty(Action.class); + } + + /** + * Sets the type of action to invoke when the alarm is triggered. + * @param action the action or null to remove + * @see RFC 5545 + * p.132-3 + * @see RFC 2445 + * p.126 + */ + public void setAction(Action action) { + setProperty(Action.class, action); + } + + /** + * Gets the length of the pause between alarm repetitions. + * @return the duration or null if not set + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public DurationProperty getDuration() { + return getProperty(DurationProperty.class); + } + + /** + * Sets the length of the pause between alarm repetitions. + * @param duration the duration or null to remove + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public void setDuration(DurationProperty duration) { + setProperty(DurationProperty.class, duration); + } + + /** + * Sets the length of the pause between alarm repetitions. + * @param duration the duration or null to remove + * @return the property that was created + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public DurationProperty setDuration(Duration duration) { + DurationProperty prop = (duration == null) ? null : new DurationProperty(duration); + setDuration(prop); + return prop; + } + + /** + * Gets the number of times an alarm should be repeated after its initial + * trigger. + * @return the repeat count or null if not set + * @see RFC 5545 + * p.133 + * @see RFC 2445 + * p.126-7 + */ + public Repeat getRepeat() { + return getProperty(Repeat.class); + } + + /** + * Sets the number of times an alarm should be repeated after its initial + * trigger. + * @param repeat the repeat count or null to remove + * @see RFC 5545 + * p.133 + * @see RFC 2445 + * p.126-7 + */ + public void setRepeat(Repeat repeat) { + setProperty(Repeat.class, repeat); + } + + /** + * Sets the number of times an alarm should be repeated after its initial + * trigger. + * @param count the repeat count (e.g. "2" to repeat it two more times after + * it was initially triggered, for a total of three times) or null to remove + * @return the property that was created + * @see RFC 5545 + * p.133 + * @see RFC 2445 + * p.126-7 + */ + public Repeat setRepeat(Integer count) { + Repeat prop = (count == null) ? null : new Repeat(count); + setRepeat(prop); + return prop; + } + + /** + * Sets the repetition information for the alarm. + * @param count the repeat count (e.g. "2" to repeat it two more times after + * it was initially triggered, for a total of three times) + * @param pauseDuration the length of the pause between repeats + * @see RFC 5545 + * p.133 + * @see RFC 2445 + * p.126-7 + */ + public void setRepeat(int count, Duration pauseDuration) { + Repeat repeat = new Repeat(count); + DurationProperty duration = new DurationProperty(pauseDuration); + setRepeat(repeat); + setDuration(duration); + } + + /** + * Gets when the alarm will be triggered. + * @return the trigger time or null if not set + * @see RFC 5545 + * p.133-6 + * @see RFC 2445 + * p.127-9 + */ + public Trigger getTrigger() { + return getProperty(Trigger.class); + } + + /** + * Sets when the alarm will be triggered. + * @param trigger the trigger time or null to remove + * @see RFC 5545 + * p.133-6 + * @see RFC 2445 + * p.127-9 + */ + public void setTrigger(Trigger trigger) { + setProperty(Trigger.class, trigger); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + checkRequiredCardinality(warnings, Action.class, Trigger.class); + + Action action = getAction(); + if (action != null) { + //AUDIO alarms should not have more than 1 attachment + if (action.isAudio()) { + if (getAttachments().size() > 1) { + warnings.add(new ValidationWarning(7)); + } + } + + //DESCRIPTION is required for DISPLAY alarms + if (action.isDisplay()) { + checkRequiredCardinality(warnings, Description.class); + } + + if (action.isEmail()) { + //SUMMARY and DESCRIPTION is required for EMAIL alarms + checkRequiredCardinality(warnings, Summary.class, Description.class); + + //EMAIL alarms must have at least 1 ATTENDEE + if (getAttendees().isEmpty()) { + warnings.add(new ValidationWarning(8)); + } + } else { + //only EMAIL alarms can have ATTENDEEs + if (!getAttendees().isEmpty()) { + warnings.add(new ValidationWarning(9)); + } + } + + if (action.isProcedure()) { + checkRequiredCardinality(warnings, Description.class); + } + } + + Trigger trigger = getTrigger(); + if (trigger != null) { + Related related = trigger.getRelated(); + if (related != null) { + ICalComponent parent = components.get(components.size() - 1); + + //if the TRIGGER is relative to DTSTART, confirm that DTSTART exists + if (related == Related.START && parent.getProperty(DateStart.class) == null) { + warnings.add(new ValidationWarning(11)); + } + + //if the TRIGGER is relative to DTEND, confirm that DTEND (or DUE) exists + if (related == Related.END) { + boolean noEndDate = false; + + if (parent instanceof VEvent) { + noEndDate = (parent.getProperty(DateEnd.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null)); + } else if (parent instanceof VTodo) { + noEndDate = (parent.getProperty(DateDue.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null)); + } + + if (noEndDate) { + warnings.add(new ValidationWarning(12)); + } + } + } + } + } + + @Override + public VAlarm copy() { + return new VAlarm(this); + } +} diff --git a/app/src/main/java/biweekly/component/VEvent.java b/app/src/main/java/biweekly/component/VEvent.java new file mode 100644 index 0000000000..b6c3dcfba4 --- /dev/null +++ b/app/src/main/java/biweekly/component/VEvent.java @@ -0,0 +1,1726 @@ +package biweekly.component; + +import static biweekly.property.ValuedProperty.getValue; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.property.Attachment; +import biweekly.property.Attendee; +import biweekly.property.Categories; +import biweekly.property.Classification; +import biweekly.property.Color; +import biweekly.property.Comment; +import biweekly.property.Conference; +import biweekly.property.Contact; +import biweekly.property.Created; +import biweekly.property.DateEnd; +import biweekly.property.DateStart; +import biweekly.property.DateTimeStamp; +import biweekly.property.Description; +import biweekly.property.DurationProperty; +import biweekly.property.ExceptionDates; +import biweekly.property.ExceptionRule; +import biweekly.property.Geo; +import biweekly.property.Image; +import biweekly.property.LastModified; +import biweekly.property.Location; +import biweekly.property.Method; +import biweekly.property.Organizer; +import biweekly.property.Priority; +import biweekly.property.RecurrenceDates; +import biweekly.property.RecurrenceId; +import biweekly.property.RecurrenceRule; +import biweekly.property.RelatedTo; +import biweekly.property.RequestStatus; +import biweekly.property.Resources; +import biweekly.property.Sequence; +import biweekly.property.Status; +import biweekly.property.Summary; +import biweekly.property.Transparency; +import biweekly.property.Uid; +import biweekly.property.Url; +import biweekly.util.Duration; +import biweekly.util.Google2445Utils; +import biweekly.util.ICalDate; +import biweekly.util.Period; +import biweekly.util.Recurrence; +import biweekly.util.com.google.ical.compat.javautil.DateIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a scheduled activity, such as a two hour meeting. + *

+ *

+ * Examples: + *

+ * + *
+ * VEvent event = new VEvent();
+ * Date start = ...
+ * event.setDateStart(start);
+ * Date end = ...
+ * event.setDateEnd(end);
+ * event.setSummary("Team Meeting");
+ * event.setLocation("Room 21C");
+ * event.setCreated(new Date());
+ * event.setRecurrenceRule(new Recurrence.Builder(Frequency.WEEKLY).build());
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.52-5 + * @see RFC 2445 p.52-4 + * @see vCal 1.0 p.13 + */ +public class VEvent extends ICalComponent { + /** + *

+ * Creates a new event. + *

+ *

+ * The following properties are added to the component when it is created: + *

+ *
    + *
  • {@link Uid}: Set to a UUID.
  • + *
  • {@link DateTimeStamp}: Set to the current time.
  • + *
+ */ + public VEvent() { + setUid(Uid.random()); + setDateTimeStamp(new Date()); + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public VEvent(VEvent original) { + super(original); + } + + /** + * Gets the unique identifier for this event. This component object comes + * populated with a UID on creation. This is a required property. + * @return the UID or null if not set + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + */ + public Uid getUid() { + return getProperty(Uid.class); + } + + /** + * Sets the unique identifier for this event. This component object comes + * populated with a UID on creation. This is a required property. + * @param uid the UID or null to remove + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + */ + public void setUid(Uid uid) { + setProperty(Uid.class, uid); + } + + /** + * Sets the unique identifier for this event. This component object comes + * populated with a UID on creation. This is a required property. + * @param uid the UID or null to remove + * @return the property that was created + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + */ + public Uid setUid(String uid) { + Uid prop = (uid == null) ? null : new Uid(uid); + setUid(prop); + return prop; + } + + /** + * Gets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the event was + * last modified (the {@link LastModified} property also holds this + * information). This event object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @return the date time stamp or null if not set + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp getDateTimeStamp() { + return getProperty(DateTimeStamp.class); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the event was + * last modified (the {@link LastModified} property also holds this + * information). This event object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public void setDateTimeStamp(DateTimeStamp dateTimeStamp) { + setProperty(DateTimeStamp.class, dateTimeStamp); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the event was + * last modified (the {@link LastModified} property also holds this + * information). This event object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @return the property that was created + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp setDateTimeStamp(Date dateTimeStamp) { + DateTimeStamp prop = (dateTimeStamp == null) ? null : new DateTimeStamp(dateTimeStamp); + setDateTimeStamp(prop); + return prop; + } + + /** + * Gets the date that the event starts. + * @return the start date or null if not set + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public DateStart getDateStart() { + return getProperty(DateStart.class); + } + + /** + * Sets the date that the event starts (required if no {@link Method} + * property is defined). + * @param dateStart the start date or null to remove + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public void setDateStart(DateStart dateStart) { + setProperty(DateStart.class, dateStart); + } + + /** + * Sets the date that the event starts (required if no {@link Method} + * property is defined). + * @param dateStart the start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public DateStart setDateStart(Date dateStart) { + return setDateStart(dateStart, true); + } + + /** + * Sets the date that the event starts (required if no {@link Method} + * property is defined). + * @param dateStart the start date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public DateStart setDateStart(Date dateStart, boolean hasTime) { + DateStart prop = (dateStart == null) ? null : new DateStart(dateStart, hasTime); + setDateStart(prop); + return prop; + } + + /** + * Gets the level of sensitivity of the event data. If not specified, the + * data within the event should be considered "public". + * @return the classification level or null if not set + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ + public Classification getClassification() { + return getProperty(Classification.class); + } + + /** + * Sets the level of sensitivity of the event data. If not specified, the + * data within the event should be considered "public". + * @param classification the classification level or null to remove + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ + public void setClassification(Classification classification) { + setProperty(Classification.class, classification); + } + + /** + * Sets the level of sensitivity of the event data. If not specified, the + * data within the event should be considered "public". + * @param classification the classification level (e.g. "CONFIDENTIAL") or + * null to remove + * @return the property that was created + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ + public Classification setClassification(String classification) { + Classification prop = (classification == null) ? null : new Classification(classification); + setClassification(prop); + return prop; + } + + /** + * Gets a detailed description of the event. The description should be more + * detailed than the one provided by the {@link Summary} property. + * @return the description or null if not set + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + * @see vCal 1.0 p.30 + */ + public Description getDescription() { + return getProperty(Description.class); + } + + /** + * Sets a detailed description of the event. The description should be more + * detailed than the one provided by the {@link Summary} property. + * @param description the description or null to remove + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + * @see vCal 1.0 p.30 + */ + public void setDescription(Description description) { + setProperty(Description.class, description); + } + + /** + * Sets a detailed description of the event. The description should be more + * detailed than the one provided by the {@link Summary} property. + * @param description the description or null to remove + * @return the property that was created + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + * @see vCal 1.0 p.30 + */ + public Description setDescription(String description) { + Description prop = (description == null) ? null : new Description(description); + setDescription(prop); + return prop; + } + + /** + * Gets a set of geographical coordinates. + * @return the geographical coordinates or null if not set + * @see RFC 5545 + * p.85-7 + * @see RFC 2445 + * p.82-3 + */ + //Note: vCal 1.0 spec is omitted because GEO is not used in vCal 1.0 events + public Geo getGeo() { + return getProperty(Geo.class); + } + + /** + * Sets a set of geographical coordinates. + * @param geo the geographical coordinates or null to remove + * @see RFC 5545 + * p.85-7 + * @see RFC 2445 + * p.82-3 + */ + //Note: vCal 1.0 spec is omitted because GEO is not used in vCal 1.0 events + public void setGeo(Geo geo) { + setProperty(Geo.class, geo); + } + + /** + * Gets the physical location of the event. + * @return the location or null if not set + * @see RFC 5545 + * p.87-8 + * @see RFC 2445 + * p.84 + * @see vCal 1.0 p.32 + */ + public Location getLocation() { + return getProperty(Location.class); + } + + /** + * Sets the physical location of the event. + * @param location the location or null to remove + * @see RFC 5545 + * p.87-8 + * @see RFC 2445 + * p.84 + * @see vCal 1.0 p.32 + */ + public void setLocation(Location location) { + setProperty(Location.class, location); + } + + /** + * Sets the physical location of the event. + * @param location the location (e.g. "Room 101") or null to remove + * @return the property that was created + * @see RFC 5545 + * p.87-8 + * @see RFC 2445 + * p.84 + * @see vCal 1.0 p.32 + */ + public Location setLocation(String location) { + Location prop = (location == null) ? null : new Location(location); + setLocation(prop); + return prop; + } + + /** + * Gets the priority of the event. + * @return the priority or null if not set + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 + * p.85-7 + * @see vCal 1.0 p.33 + */ + public Priority getPriority() { + return getProperty(Priority.class); + } + + /** + * Sets the priority of the event. + * @param priority the priority or null to remove + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 + * p.85-7 + * @see vCal 1.0 p.33 + */ + public void setPriority(Priority priority) { + setProperty(Priority.class, priority); + } + + /** + * Sets the priority of the event. + * @param priority the priority ("0" is undefined, "1" is the highest, "9" + * is the lowest) or null to remove + * @return the property that was created + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 + * p.85-7 + * @see vCal 1.0 p.33 + */ + public Priority setPriority(Integer priority) { + Priority prop = (priority == null) ? null : new Priority(priority); + setPriority(prop); + return prop; + } + + /** + * Gets the status of the event. + * @return the status or null if not set + * @see RFC 5545 + * p.92-3 + * @see RFC 2445 + * p.88-9 + * @see vCal 1.0 p.35-6 + */ + public Status getStatus() { + return getProperty(Status.class); + } + + /** + *

+ * Sets the status of the event. + *

+ *

+ * Valid event status codes are: + *

+ *
    + *
  • TENTATIVE
  • + *
  • CONFIRMED
  • + *
  • CANCELLED
  • + *
+ * @param status the status or null to remove + * @see RFC 5545 + * p.92-3 + * @see RFC 2445 + * p.88-9 + * @see vCal 1.0 p.35-6 + */ + public void setStatus(Status status) { + setProperty(Status.class, status); + } + + /** + * Gets the summary of the event. + * @return the summary or null if not set + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ + public Summary getSummary() { + return getProperty(Summary.class); + } + + /** + * Sets the summary of the event. + * @param summary the summary or null to remove + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ + public void setSummary(Summary summary) { + setProperty(Summary.class, summary); + } + + /** + * Sets the summary of the event. + * @param summary the summary or null to remove + * @return the property that was created + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ + public Summary setSummary(String summary) { + Summary prop = (summary == null) ? null : new Summary(summary); + setSummary(prop); + return prop; + } + + /** + * Gets whether an event is visible to free/busy time searches. If the event + * does not have this property, it should be considered visible ("opaque"). + * @return the transparency or null if not set + * @see RFC 5545 + * p.101-2 + * @see RFC 2445 + * p.96-7 + * @see vCal 1.0 p.36-7 + */ + public Transparency getTransparency() { + return getProperty(Transparency.class); + } + + /** + * Sets whether an event is visible to free/busy time searches. + * @param transparency the transparency or null to remove + * @see RFC 5545 + * p.101-2 + * @see RFC 2445 + * p.96-7 + * @see vCal 1.0 p.36-7 + */ + public void setTransparency(Transparency transparency) { + setProperty(Transparency.class, transparency); + } + + /** + * Sets whether an event is visible to free/busy time searches. + * @param transparent true to hide the event, false to make it visible it, + * or null to remove the property + * @return the property that was created + * @see RFC 5545 + * p.101-2 + * @see RFC 2445 + * p.96-7 + * @see vCal 1.0 p.36-7 + */ + public Transparency setTransparency(Boolean transparent) { + Transparency prop = null; + if (transparent != null) { + prop = transparent ? Transparency.transparent() : Transparency.opaque(); + } + setTransparency(prop); + return prop; + } + + /** + * Gets the organizer of the event. + * @return the organizer or null if not set + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer getOrganizer() { + return getProperty(Organizer.class); + } + + /** + * Sets the organizer of the event. + * @param organizer the organizer or null to remove + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public void setOrganizer(Organizer organizer) { + setProperty(Organizer.class, organizer); + } + + /** + * Sets the organizer of the event. + * @param email the organizer's email address (e.g. "johndoe@example.com") + * or null to remove + * @return the property that was created + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer setOrganizer(String email) { + Organizer prop = (email == null) ? null : new Organizer(null, email); + setOrganizer(prop); + return prop; + } + + /** + * Gets the original value of the {@link DateStart} property if the event is + * recurring and has been modified. Used in conjunction with the {@link Uid} + * and {@link Sequence} properties to uniquely identify a recurrence + * instance. + * @return the recurrence ID or null if not set + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public RecurrenceId getRecurrenceId() { + return getProperty(RecurrenceId.class); + } + + /** + * Sets the original value of the {@link DateStart} property if the event is + * recurring and has been modified. Used in conjunction with the {@link Uid} + * and {@link Sequence} properties to uniquely identify a recurrence + * instance. + * @param recurrenceId the recurrence ID or null to remove + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public void setRecurrenceId(RecurrenceId recurrenceId) { + setProperty(RecurrenceId.class, recurrenceId); + } + + /** + * Sets the original value of the {@link DateStart} property if the event is + * recurring and has been modified. Used in conjunction with the {@link Uid} + * and {@link Sequence} properties to uniquely identify a recurrence + * instance. + * @param originalStartDate the original start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public RecurrenceId setRecurrenceId(Date originalStartDate) { + RecurrenceId prop = (originalStartDate == null) ? null : new RecurrenceId(originalStartDate); + setRecurrenceId(prop); + return prop; + } + + /** + * Gets a URL to a resource that contains additional information about the + * event. + * @return the URL or null if not set + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + */ + public Url getUrl() { + return getProperty(Url.class); + } + + /** + * Sets a URL to a resource that contains additional information about the + * event. + * @param url the URL or null to remove + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + */ + public void setUrl(Url url) { + setProperty(Url.class, url); + } + + /** + * Sets a URL to a resource that contains additional information about the + * event. + * @param url the URL (e.g. "http://example.com/resource.ics") or null to + * remove + * @return the property that was created + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + */ + public Url setUrl(String url) { + Url prop = (url == null) ? null : new Url(url); + setUrl(prop); + return prop; + } + + /** + * Gets how often the event repeats. + * @return the recurrence rule or null if not set + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ + public RecurrenceRule getRecurrenceRule() { + return getProperty(RecurrenceRule.class); + } + + /** + * Sets how often the event repeats. + * @param recur the recurrence rule or null to remove + * @return the property that was created + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ + public RecurrenceRule setRecurrenceRule(Recurrence recur) { + RecurrenceRule prop = (recur == null) ? null : new RecurrenceRule(recur); + setRecurrenceRule(prop); + return prop; + } + + /** + * Sets how often the event repeats. + * @param recurrenceRule the recurrence rule or null to remove + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ + public void setRecurrenceRule(RecurrenceRule recurrenceRule) { + setProperty(RecurrenceRule.class, recurrenceRule); + } + + /** + * Gets the date that the event ends. + * @return the end date or null if not set + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + * @see vCal 1.0 p.31 + */ + public DateEnd getDateEnd() { + return getProperty(DateEnd.class); + } + + /** + * Sets the date that the event ends. This must NOT be set if a + * {@link DurationProperty} is defined. + * @param dateEnd the end date or null to remove + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + * @see vCal 1.0 p.31 + */ + public void setDateEnd(DateEnd dateEnd) { + setProperty(DateEnd.class, dateEnd); + } + + /** + * Sets the date that the event ends. This must NOT be set if a + * {@link DurationProperty} is defined. + * @param dateEnd the end date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + * @see vCal 1.0 p.31 + */ + public DateEnd setDateEnd(Date dateEnd) { + return setDateEnd(dateEnd, true); + } + + /** + * Sets the date that the event ends. This must NOT be set if a + * {@link DurationProperty} is defined. + * @param dateEnd the end date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + * @see vCal 1.0 p.31 + */ + public DateEnd setDateEnd(Date dateEnd, boolean hasTime) { + DateEnd prop = (dateEnd == null) ? null : new DateEnd(dateEnd, hasTime); + setDateEnd(prop); + return prop; + } + + /** + * Gets the duration of the event. + * @return the duration or null if not set + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public DurationProperty getDuration() { + return getProperty(DurationProperty.class); + } + + /** + * Sets the duration of the event. This must NOT be set if a {@link DateEnd} + * is defined. + * @param duration the duration or null to remove + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public void setDuration(DurationProperty duration) { + setProperty(DurationProperty.class, duration); + } + + /** + * Sets the duration of the event. This must NOT be set if a {@link DateEnd} + * is defined. + * @param duration the duration or null to remove + * @return the property that was created + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public DurationProperty setDuration(Duration duration) { + DurationProperty prop = (duration == null) ? null : new DurationProperty(duration); + setDuration(prop); + return prop; + } + + /** + * Gets the date-time that the event was initially created. + * @return the creation date-time or null if not set + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ + public Created getCreated() { + return getProperty(Created.class); + } + + /** + * Sets the date-time that the event was initially created. + * @param created the creation date-time or null to remove + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ + public void setCreated(Created created) { + setProperty(Created.class, created); + } + + /** + * Sets the date-time that the event was initially created. + * @param created the creation date-time or null to remove + * @return the property that was created + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ + public Created setCreated(Date created) { + Created prop = (created == null) ? null : new Created(created); + setCreated(prop); + return prop; + } + + /** + * Gets the date-time that the event was last changed. + * @return the last modified date or null if not set + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + * @see vCal 1.0 p.31 + */ + public LastModified getLastModified() { + return getProperty(LastModified.class); + } + + /** + * Sets the date-time that event was last changed. + * @param lastModified the last modified date or null to remove + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + * @see vCal 1.0 p.31 + */ + public void setLastModified(LastModified lastModified) { + setProperty(LastModified.class, lastModified); + } + + /** + * Sets the date-time that the event was last changed. + * @param lastModified the last modified date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + * @see vCal 1.0 p.31 + */ + public LastModified setLastModified(Date lastModified) { + LastModified prop = (lastModified == null) ? null : new LastModified(lastModified); + setLastModified(prop); + return prop; + } + + /** + * Gets the revision number of the event. The organizer can increment this + * number every time he or she makes a significant change. + * @return the sequence number + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public Sequence getSequence() { + return getProperty(Sequence.class); + } + + /** + * Sets the revision number of the event. The organizer can increment this + * number every time he or she makes a significant change. + * @param sequence the sequence number + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public void setSequence(Sequence sequence) { + setProperty(Sequence.class, sequence); + } + + /** + * Sets the revision number of the event. The organizer can increment this + * number every time he or she makes a significant change. + * @param sequence the sequence number + * @return the property that was created + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public Sequence setSequence(Integer sequence) { + Sequence prop = (sequence == null) ? null : new Sequence(sequence); + setSequence(prop); + return prop; + } + + /** + * Increments the revision number of the event. The organizer can increment + * this number every time he or she makes a significant change. + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public void incrementSequence() { + Sequence sequence = getSequence(); + if (sequence == null) { + setSequence(1); + } else { + sequence.increment(); + } + } + + /** + * Gets any attachments that are associated with the event. + * @return the attachments (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + * @see vCal 1.0 p.25 + */ + public List getAttachments() { + return getProperties(Attachment.class); + } + + /** + * Adds an attachment to the event. + * @param attachment the attachment to add + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + * @see vCal 1.0 p.25 + */ + public void addAttachment(Attachment attachment) { + addProperty(attachment); + } + + /** + * Gets the people who are attending the event. + * @return the attendees (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ + public List getAttendees() { + return getProperties(Attendee.class); + } + + /** + * Adds a person who is attending the event. + * @param attendee the attendee + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ + public void addAttendee(Attendee attendee) { + addProperty(attendee); + } + + /** + * Adds a person who is attending the event. + * @param email the attendee's email address + * @return the property that was created + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ + public Attendee addAttendee(String email) { + Attendee prop = new Attendee(null, email); + addAttendee(prop); + return prop; + } + + /** + * Gets a list of "tags" or "keywords" that describe the event. + * @return the categories (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public List getCategories() { + return getProperties(Categories.class); + } + + /** + * Adds a list of "tags" or "keywords" that describe the event. Note that a + * single property can hold multiple keywords. + * @param categories the categories to add + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public void addCategories(Categories categories) { + addProperty(categories); + } + + /** + * Adds a list of "tags" or "keywords" that describe the event. + * @param categories the categories to add + * @return the property that was created + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public Categories addCategories(String... categories) { + Categories prop = new Categories(categories); + addCategories(prop); + return prop; + } + + /** + * Adds a list of "tags" or "keywords" that describe the event. + * @param categories the categories to add + * @return the property that was created + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public Categories addCategories(List categories) { + Categories prop = new Categories(categories); + addCategories(prop); + return prop; + } + + /** + * Gets the comments attached to the event. + * @return the comments (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public List getComments() { + return getProperties(Comment.class); + } + + /** + * Adds a comment to the event. + * @param comment the comment to add + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public void addComment(Comment comment) { + addProperty(comment); + } + + /** + * Adds a comment to the event. + * @param comment the comment to add + * @return the property that was created + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public Comment addComment(String comment) { + Comment prop = new Comment(comment); + addComment(prop); + return prop; + } + + /** + * Gets the contacts associated with the event. + * @return the contacts (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public List getContacts() { + return getProperties(Contact.class); + } + + /** + * Adds a contact to the event. + * @param contact the contact + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public void addContact(Contact contact) { + addProperty(contact); + } + + /** + * Adds a contact to the event. + * @param contact the contact (e.g. "ACME Co - (123) 555-1234") + * @return the property that was created + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public Contact addContact(String contact) { + Contact prop = new Contact(contact); + addContact(prop); + return prop; + } + + /** + * Gets the list of exceptions to the recurrence rule defined in the event + * (if one is defined). + * @return the list of exceptions (any changes made this list will affect + * the parent component object and vice versa) + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + * @see vCal 1.0 p.31 + */ + public List getExceptionDates() { + return getProperties(ExceptionDates.class); + } + + /** + * Adds a list of exceptions to the recurrence rule defined in the event (if + * one is defined). Note that this property can contain multiple dates. + * @param exceptionDates the list of exceptions + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + * @see vCal 1.0 p.31 + */ + public void addExceptionDates(ExceptionDates exceptionDates) { + addProperty(exceptionDates); + } + + /** + * Gets the response to a scheduling request. + * @return the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public RequestStatus getRequestStatus() { + return getProperty(RequestStatus.class); + } + + /** + * Sets the response to a scheduling request. + * @param requestStatus the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public void setRequestStatus(RequestStatus requestStatus) { + setProperty(RequestStatus.class, requestStatus); + } + + /** + * Gets the components that the event is related to. + * @return the relationships (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ + public List getRelatedTo() { + return getProperties(RelatedTo.class); + } + + /** + * Adds a component that the event is related to. + * @param relatedTo the relationship + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ + public void addRelatedTo(RelatedTo relatedTo) { + //TODO create a method that accepts a component and make the RelatedTo property invisible to the user + //@formatter:off + /* + * addRelation(RelationshipType relType, ICalComponent component) { + * RelatedTo prop = new RelatedTo(component.getUid().getValue()); + * prop.setRelationshipType(relType); + * addProperty(prop); + * } + */ + //@formatter:on + addProperty(relatedTo); + } + + /** + * Adds a component that the event is related to. + * @param uid the UID of the other component + * @return the property that was created + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ + public RelatedTo addRelatedTo(String uid) { + RelatedTo prop = new RelatedTo(uid); + addRelatedTo(prop); + return prop; + } + + /** + * Gets the resources that are needed for the event. + * @return the resources (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public List getResources() { + return getProperties(Resources.class); + } + + /** + * Adds a list of resources that are needed for the event. Note that a + * single property can hold multiple resources. + * @param resources the resources to add + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public void addResources(Resources resources) { + addProperty(resources); + } + + /** + * Adds a list of resources that are needed for the event. + * @param resources the resources to add (e.g. "easel", "projector") + * @return the property that was created + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public Resources addResources(String... resources) { + Resources prop = new Resources(resources); + addResources(prop); + return prop; + } + + /** + * Adds a list of resources that are needed for the event. + * @param resources the resources to add (e.g. "easel", "projector") + * @return the property that was created + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public Resources addResources(List resources) { + Resources prop = new Resources(resources); + addResources(prop); + return prop; + } + + /** + * Gets the list of dates/periods that help define the recurrence rule of + * this event (if one is defined). + * @return the recurrence dates (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + * @see vCal 1.0 p.34 + */ + public List getRecurrenceDates() { + return getProperties(RecurrenceDates.class); + } + + /** + * Adds a list of dates/periods that help define the recurrence rule of this + * event (if one is defined). + * @param recurrenceDates the recurrence dates + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + * @see vCal 1.0 p.34 + */ + public void addRecurrenceDates(RecurrenceDates recurrenceDates) { + addProperty(recurrenceDates); + } + + /** + * Gets the alarms that are assigned to this event. + * @return the alarms (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.71-6 + * @see RFC 2445 + * p.67-73 + */ + public List getAlarms() { + return getComponents(VAlarm.class); + } + + /** + * Adds an alarm to this event. + * @param alarm the alarm + * @see RFC 5545 + * p.71-6 + * @see RFC 2445 + * p.67-73 + */ + public void addAlarm(VAlarm alarm) { + addComponent(alarm); + } + + /** + *

+ * Gets the exceptions for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @return the exception rules (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ + public List getExceptionRules() { + return getProperties(ExceptionRule.class); + } + + /** + *

+ * Adds an exception for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @param recur the exception rule to add + * @return the property that was created + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ + public ExceptionRule addExceptionRule(Recurrence recur) { + ExceptionRule prop = new ExceptionRule(recur); + addExceptionRule(prop); + return prop; + } + + /** + *

+ * Adds an exception for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @param exceptionRule the exception rule to add + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ + public void addExceptionRule(ExceptionRule exceptionRule) { + addProperty(exceptionRule); + } + + /** + * Gets the color that clients may use when displaying the event (for + * example, a background color). + * @return the property or null if not set + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color getColor() { + return getProperty(Color.class); + } + + /** + * Sets the color that clients may use when displaying the event (for + * example, a background color). + * @param color the property or null to remove + * @see draft-ietf-calext-extensions-01 + * p.79 + */ + public void setColor(Color color) { + setProperty(Color.class, color); + } + + /** + * Sets the color that clients may use when displaying the event (for + * example, a background color). + * @param color the color name (case insensitive) or null to remove. + * Acceptable values are defined in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color setColor(String color) { + Color prop = (color == null) ? null : new Color(color); + setColor(prop); + return prop; + } + + /** + * Gets the images that are associated with the event. + * @return the images (any changes made this list will affect the parent + * component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public List getImages() { + return getProperties(Image.class); + } + + /** + * Adds an image that is associated with the event. + * @param image the image + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public void addImage(Image image) { + addProperty(image); + } + + /** + * Gets information related to the event's conference system. + * @return the conferences (any changes made this list will affect the + * parent component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.11 + */ + public List getConferences() { + return getProperties(Conference.class); + } + + /** + * Adds information related to the event's conference system. + * @param conference the conference + * @see draft-ietf-calext-extensions-01 + * p.11 + */ + public void addConference(Conference conference) { + addProperty(conference); + } + + /** + *

+ * Creates an iterator that computes the dates defined by the + * {@link RecurrenceRule} and {@link RecurrenceDates} properties (if + * present), and excludes those dates which are defined by the + * {@link ExceptionRule} and {@link ExceptionDates} properties (if present). + *

+ *

+ * In order for {@link RecurrenceRule} and {@link ExceptionRule} properties + * to be included in this iterator, a {@link DateStart} property must be + * defined. + *

+ *

+ * {@link Period} values in {@link RecurrenceDates} properties are not + * supported and are ignored. + *

+ * @param timezone the timezone to iterate in. This is needed in order to + * adjust for when the iterator passes over a daylight savings boundary. + * This parameter is ignored if the start date does not have a time + * component. + * @return the iterator + */ + public DateIterator getDateIterator(TimeZone timezone) { + return Google2445Utils.getDateIterator(this, timezone); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version != ICalVersion.V1_0) { + checkRequiredCardinality(warnings, Uid.class, DateTimeStamp.class); + checkOptionalCardinality(warnings, Classification.class, Created.class, Description.class, Geo.class, LastModified.class, Location.class, Organizer.class, Priority.class, Status.class, Summary.class, Transparency.class, Url.class, RecurrenceId.class); + } + + checkOptionalCardinality(warnings, Color.class); + + Status[] validStatuses; + switch (version) { + case V1_0: + validStatuses = new Status[] { Status.tentative(), Status.confirmed(), Status.declined(), Status.needsAction(), Status.sent(), Status.delegated() }; + break; + default: + validStatuses = new Status[] { Status.tentative(), Status.confirmed(), Status.cancelled() }; + break; + } + checkStatus(warnings, validStatuses); + + ICalDate dateStart = getValue(getDateStart()); + ICalDate dateEnd = getValue(getDateEnd()); + + //DTSTART is always required, unless there is a METHOD property at the iCal root + ICalComponent ical = components.get(0); + if (version != ICalVersion.V1_0 && dateStart == null && ical.getProperty(Method.class) == null) { + warnings.add(new ValidationWarning(14)); + } + + //DTSTART is required if DTEND exists + if (dateEnd != null && dateStart == null) { + warnings.add(new ValidationWarning(15)); + } + + if (dateStart != null && dateEnd != null) { + //DTSTART must come before DTEND + if (dateStart.compareTo(dateEnd) > 0) { + warnings.add(new ValidationWarning(16)); + } + + //DTSTART and DTEND must have the same data type + if (dateStart.hasTime() != dateEnd.hasTime()) { + warnings.add(new ValidationWarning(17)); + } + } + + //DTEND and DURATION cannot both exist + if (dateEnd != null && getDuration() != null) { + warnings.add(new ValidationWarning(18)); + } + + //DTSTART and RECURRENCE-ID must have the same data type + ICalDate recurrenceId = getValue(getRecurrenceId()); + if (recurrenceId != null && dateStart != null && dateStart.hasTime() != recurrenceId.hasTime()) { + warnings.add(new ValidationWarning(19)); + } + + //BYHOUR, BYMINUTE, and BYSECOND cannot be specified in RRULE if DTSTART's data type is "date" + //RFC 5545 p. 167 + Recurrence rrule = getValue(getRecurrenceRule()); + if (dateStart != null && rrule != null) { + if (!dateStart.hasTime() && (!rrule.getByHour().isEmpty() || !rrule.getByMinute().isEmpty() || !rrule.getBySecond().isEmpty())) { + warnings.add(new ValidationWarning(5)); + } + } + + //there *should* be only 1 instance of RRULE + //RFC 5545 p. 167 + if (getProperties(RecurrenceRule.class).size() > 1) { + warnings.add(new ValidationWarning(6)); + } + } + + @Override + public VEvent copy() { + return new VEvent(this); + } +} diff --git a/app/src/main/java/biweekly/component/VFreeBusy.java b/app/src/main/java/biweekly/component/VFreeBusy.java new file mode 100644 index 0000000000..c3afbba9e5 --- /dev/null +++ b/app/src/main/java/biweekly/component/VFreeBusy.java @@ -0,0 +1,655 @@ +package biweekly.component; + +import static biweekly.property.ValuedProperty.getValue; + +import java.util.Date; +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.parameter.FreeBusyType; +import biweekly.property.Attendee; +import biweekly.property.Comment; +import biweekly.property.Contact; +import biweekly.property.DateEnd; +import biweekly.property.DateStart; +import biweekly.property.DateTimeStamp; +import biweekly.property.FreeBusy; +import biweekly.property.LastModified; +import biweekly.property.Method; +import biweekly.property.Organizer; +import biweekly.property.RequestStatus; +import biweekly.property.Uid; +import biweekly.property.Url; +import biweekly.util.Duration; +import biweekly.util.ICalDate; +import biweekly.util.Period; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a collection of time ranges that describe when a person is available + * and unavailable. + *

+ *

+ * Examples: + *

+ * + *
+ * VFreeBusy freebusy = new VFreeBusy();
+ * 
+ * Date start = ...
+ * Date end = ...
+ * freebusy.addFreeBusy(FreeBusyType.FREE, start, end);
+ * 
+ * start = ...
+ * Duration duration = Duration.builder().hours(2).build();
+ * freebusy.addFreeBusy(FreeBusyType.BUSY, start, duration);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.59-62 + * @see RFC 2445 + * p.58-60 + */ +/* + * Note: References to the vCal 1.0 spec are omitted from the property + * getter/setter method Javadocs because vCal does not use the VFREEBUSY + * component. + */ +public class VFreeBusy extends ICalComponent { + /** + *

+ * Creates a new free/busy component. + *

+ *

+ * The following properties are added to the component when it is created: + *

+ *
    + *
  • {@link Uid}: Set to a UUID.
  • + *
  • {@link DateTimeStamp}: Set to the current time.
  • + *
+ */ + public VFreeBusy() { + setUid(Uid.random()); + setDateTimeStamp(new Date()); + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public VFreeBusy(VFreeBusy original) { + super(original); + } + + /** + * Gets the unique identifier for this free/busy entry. This component + * object comes populated with a UID on creation. This is a required + * property. + * @return the UID or null if not set + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + */ + public Uid getUid() { + return getProperty(Uid.class); + } + + /** + * Sets the unique identifier for this free/busy entry. This component + * object comes populated with a UID on creation. This is a required + * property. + * @param uid the UID or null to remove + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + */ + public void setUid(Uid uid) { + setProperty(Uid.class, uid); + } + + /** + * Sets the unique identifier for this free/busy entry. This component + * object comes populated with a UID on creation. This is a required + * property. + * @param uid the UID or null to remove + * @return the property that was created + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + */ + public Uid setUid(String uid) { + Uid prop = (uid == null) ? null : new Uid(uid); + setUid(prop); + return prop; + } + + /** + * Gets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the free/busy + * entry was last modified (the {@link LastModified} property also holds + * this information). This free/busy object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @return the date time stamp or null if not set + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp getDateTimeStamp() { + return getProperty(DateTimeStamp.class); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the free/busy + * entry was last modified (the {@link LastModified} property also holds + * this information). This free/busy object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public void setDateTimeStamp(DateTimeStamp dateTimeStamp) { + setProperty(DateTimeStamp.class, dateTimeStamp); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the free/busy + * entry was last modified (the {@link LastModified} property also holds + * this information). This free/busy object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @return the property that was created + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp setDateTimeStamp(Date dateTimeStamp) { + DateTimeStamp prop = (dateTimeStamp == null) ? null : new DateTimeStamp(dateTimeStamp); + setDateTimeStamp(prop); + return prop; + } + + /** + * Gets the contact associated with the free/busy entry. + * @return the contact or null if not set + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public Contact getContact() { + return getProperty(Contact.class); + } + + /** + * Sets the contact for the free/busy entry. + * @param contact the contact or null to remove + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public void setContact(Contact contact) { + setProperty(Contact.class, contact); + } + + /** + * Sets the contact for the free/busy entry. + * @param contact the contact (e.g. "ACME Co - (123) 555-1234") + * @return the property that was created + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public Contact addContact(String contact) { + Contact prop = new Contact(contact); + setContact(prop); + return prop; + } + + /** + * Gets the date that the free/busy entry starts. + * @return the start date or null if not set + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart getDateStart() { + return getProperty(DateStart.class); + } + + /** + * Sets the date that the free/busy entry starts. + * @param dateStart the start date or null to remove + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public void setDateStart(DateStart dateStart) { + setProperty(DateStart.class, dateStart); + } + + /** + * Sets the date that the free/busy entry starts. + * @param dateStart the start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart setDateStart(Date dateStart) { + return setDateStart(dateStart, true); + } + + /** + * Sets the date that the free/busy entry starts. + * @param dateStart the start date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart setDateStart(Date dateStart, boolean hasTime) { + DateStart prop = (dateStart == null) ? null : new DateStart(dateStart, hasTime); + setDateStart(prop); + return prop; + } + + /** + * Gets the date that the free/busy entry ends. + * @return the end date or null if not set + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + */ + public DateEnd getDateEnd() { + return getProperty(DateEnd.class); + } + + /** + * Sets the date that the free/busy entry ends. + * @param dateEnd the end date or null to remove + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + */ + public void setDateEnd(DateEnd dateEnd) { + setProperty(DateEnd.class, dateEnd); + } + + /** + * Sets the date that the free/busy entry ends. + * @param dateEnd the end date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + */ + public DateEnd setDateEnd(Date dateEnd) { + return setDateEnd(dateEnd, true); + } + + /** + * Sets the date that the free/busy entry ends. + * @param dateEnd the end date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.95-6 + * @see RFC 2445 + * p.91-2 + */ + public DateEnd setDateEnd(Date dateEnd, boolean hasTime) { + DateEnd prop = (dateEnd == null) ? null : new DateEnd(dateEnd, hasTime); + setDateEnd(prop); + return prop; + } + + /** + * Gets the person requesting the free/busy time. + * @return the person requesting the free/busy time or null if not set + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer getOrganizer() { + return getProperty(Organizer.class); + } + + /** + * Sets the person requesting the free/busy time. + * @param organizer the person requesting the free/busy time or null to + * remove + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public void setOrganizer(Organizer organizer) { + setProperty(Organizer.class, organizer); + } + + /** + * Sets the person requesting the free/busy time. + * @param email the email address of the person requesting the free/busy + * time (e.g. "johndoe@example.com") or null to remove + * @return the property that was created + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer setOrganizer(String email) { + Organizer prop = (email == null) ? null : new Organizer(null, email); + setOrganizer(prop); + return prop; + } + + /** + * Gets a URL to a resource that contains additional information about the + * free/busy entry. + * @return the URL or null if not set + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + */ + public Url getUrl() { + return getProperty(Url.class); + } + + /** + * Sets a URL to a resource that contains additional information about the + * free/busy entry. + * @param url the URL or null to remove + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + */ + public void setUrl(Url url) { + setProperty(Url.class, url); + } + + /** + * Sets a URL to a resource that contains additional information about the + * free/busy entry. + * @param url the URL (e.g. "http://example.com/resource.ics") or null to + * remove + * @return the property that was created + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + */ + public Url setUrl(String url) { + Url prop = (url == null) ? null : new Url(url); + setUrl(prop); + return prop; + } + + /** + * Gets the people who are involved in the free/busy entry. + * @return the attendees (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public List getAttendees() { + return getProperties(Attendee.class); + } + + /** + * Adds a person who is involved in the free/busy entry. + * @param attendee the attendee + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public void addAttendee(Attendee attendee) { + addProperty(attendee); + } + + /** + * Gets the comments attached to the free/busy entry. + * @return the comments (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public List getComments() { + return getProperties(Comment.class); + } + + /** + * Adds a comment to the free/busy entry. + * @param comment the comment to add + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public void addComment(Comment comment) { + addProperty(comment); + } + + /** + * Adds a comment to the free/busy entry. + * @param comment the comment to add + * @return the property that was created + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public Comment addComment(String comment) { + Comment prop = new Comment(comment); + addComment(prop); + return prop; + } + + /** + * Gets the person's availabilities over certain time periods (for example, + * "free" between 1pm-3pm, but "busy" between 3pm-4pm). + * @return the availabilities (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.100-1 + * @see RFC 2445 + * p.95-6 + */ + public List getFreeBusy() { + return getProperties(FreeBusy.class); + } + + /** + * Adds a list of time periods for which the person is free or busy (for + * example, "free" between 1pm-3pm and 4pm-5pm). Note that a + * {@link FreeBusy} property can contain multiple time periods, but only one + * availability type (e.g. "busy"). + * @param freeBusy the availabilities + * @see RFC 5545 + * p.100-1 + * @see RFC 2445 + * p.95-6 + */ + public void addFreeBusy(FreeBusy freeBusy) { + addProperty(freeBusy); + } + + /** + * Adds a single time period for which the person is free or busy (for + * example, "free" between 1pm-3pm). This method will look for an existing + * property that has the given {@link FreeBusyType} and add the time period + * to it, or create a new property is one cannot be found. + * @param type the availability type (e.g. "free" or "busy") + * @param start the start date-time + * @param end the end date-time + * @return the property that was created/modified + * @see RFC 5545 + * p.100-1 + * @see RFC 2445 + * p.95-6 + */ + public FreeBusy addFreeBusy(FreeBusyType type, Date start, Date end) { + FreeBusy found = findByFreeBusyType(type); + found.getValues().add(new Period(start, end)); + return found; + } + + /** + * Adds a single time period for which the person is free or busy (for + * example, "free" for 2 hours after 1pm). This method will look for an + * existing property that has the given {@link FreeBusyType} and add the + * time period to it, or create a new property is one cannot be found. + * @param type the availability type (e.g. "free" or "busy") + * @param start the start date-time + * @param duration the length of time + * @return the property that was created/modified + * @see RFC 5545 + * p.100-1 + * @see RFC 2445 + * p.95-6 + */ + public FreeBusy addFreeBusy(FreeBusyType type, Date start, Duration duration) { + FreeBusy found = findByFreeBusyType(type); + found.getValues().add(new Period(start, duration)); + return found; + } + + private FreeBusy findByFreeBusyType(FreeBusyType type) { + for (FreeBusy freeBusy : getFreeBusy()) { + if (freeBusy.getType() == type) { + return freeBusy; + } + } + + FreeBusy freeBusy = new FreeBusy(); + freeBusy.setType(type); + addFreeBusy(freeBusy); + return freeBusy; + } + + /** + * Gets the response to a scheduling request. + * @return the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public RequestStatus getRequestStatus() { + return getProperty(RequestStatus.class); + } + + /** + * Sets the response to a scheduling request. + * @param requestStatus the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public void setRequestStatus(RequestStatus requestStatus) { + setProperty(RequestStatus.class, requestStatus); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version == ICalVersion.V1_0) { + warnings.add(new ValidationWarning(48, version)); + } + + checkRequiredCardinality(warnings, Uid.class, DateTimeStamp.class); + checkOptionalCardinality(warnings, Contact.class, DateStart.class, DateEnd.class, Organizer.class, Url.class); + + ICalDate dateStart = getValue(getDateStart()); + ICalDate dateEnd = getValue(getDateEnd()); + + //DTSTART is required if DTEND exists + if (dateEnd != null && dateStart == null) { + warnings.add(new ValidationWarning(15)); + } + + //DTSTART and DTEND must contain a time component + if (dateStart != null && !dateStart.hasTime()) { + warnings.add(new ValidationWarning(20, DateStart.class.getSimpleName())); + } + if (dateEnd != null && !dateEnd.hasTime()) { + warnings.add(new ValidationWarning(20, DateEnd.class.getSimpleName())); + } + + //DTSTART must come before DTEND + if (dateStart != null && dateEnd != null && dateStart.compareTo(dateEnd) >= 0) { + warnings.add(new ValidationWarning(16)); + } + } + + @Override + public VFreeBusy copy() { + return new VFreeBusy(this); + } +} diff --git a/app/src/main/java/biweekly/component/VJournal.java b/app/src/main/java/biweekly/component/VJournal.java new file mode 100644 index 0000000000..0ad587dff5 --- /dev/null +++ b/app/src/main/java/biweekly/component/VJournal.java @@ -0,0 +1,1259 @@ +package biweekly.component; + +import static biweekly.property.ValuedProperty.getValue; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.property.Attachment; +import biweekly.property.Attendee; +import biweekly.property.Categories; +import biweekly.property.Classification; +import biweekly.property.Color; +import biweekly.property.Comment; +import biweekly.property.Contact; +import biweekly.property.Created; +import biweekly.property.DateStart; +import biweekly.property.DateTimeStamp; +import biweekly.property.Description; +import biweekly.property.ExceptionDates; +import biweekly.property.ExceptionRule; +import biweekly.property.Image; +import biweekly.property.LastModified; +import biweekly.property.Method; +import biweekly.property.Organizer; +import biweekly.property.RecurrenceDates; +import biweekly.property.RecurrenceId; +import biweekly.property.RecurrenceRule; +import biweekly.property.RelatedTo; +import biweekly.property.RequestStatus; +import biweekly.property.Sequence; +import biweekly.property.Status; +import biweekly.property.Summary; +import biweekly.property.Uid; +import biweekly.property.Url; +import biweekly.util.Google2445Utils; +import biweekly.util.ICalDate; +import biweekly.util.Period; +import biweekly.util.Recurrence; +import biweekly.util.com.google.ical.compat.javautil.DateIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a journal entry. + *

+ *

+ * Examples: + *

+ * + *
+ * VJournal journal = new VJournal();
+ * journal.setSummary("Team Meeting");
+ * journal.setDescription("The following items were discussed: ...");
+ * byte[] slides = ...
+ * journal.addAttachment(new Attachment("application/vnd.ms-powerpoint", slides));
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.57-9 + * @see RFC 2445 p.56-7 + */ +/* + * Note: References to the vCal 1.0 spec are omitted from the property + * getter/setter method Javadocs because vCal does not use the VJOURNAL + * component. + */ +public class VJournal extends ICalComponent { + /** + *

+ * Creates a new journal entry. + *

+ *

+ * The following properties are added to the component when it is created: + *

+ *
    + *
  • {@link Uid}: Set to a UUID.
  • + *
  • {@link DateTimeStamp}: Set to the current time.
  • + *
+ */ + public VJournal() { + setUid(Uid.random()); + setDateTimeStamp(new Date()); + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public VJournal(VJournal original) { + super(original); + } + + /** + * Gets the unique identifier for this journal entry. This component object + * comes populated with a UID on creation. This is a required + * property. + * @return the UID or null if not set + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + */ + public Uid getUid() { + return getProperty(Uid.class); + } + + /** + * Sets the unique identifier for this journal entry. This component object + * comes populated with a UID on creation. This is a required + * property. + * @param uid the UID or null to remove + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + */ + public void setUid(Uid uid) { + setProperty(Uid.class, uid); + } + + /** + * Sets the unique identifier for this journal entry. This component object + * comes populated with a UID on creation. This is a required + * property. + * @param uid the UID or null to remove + * @return the property that was created + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + */ + public Uid setUid(String uid) { + Uid prop = (uid == null) ? null : new Uid(uid); + setUid(prop); + return prop; + } + + /** + * Gets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the journal + * entry was last modified (the {@link LastModified} property also holds + * this information). This journal entry object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @return the date time stamp or null if not set + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp getDateTimeStamp() { + return getProperty(DateTimeStamp.class); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the journal + * entry was last modified (the {@link LastModified} property also holds + * this information). This journal entry object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public void setDateTimeStamp(DateTimeStamp dateTimeStamp) { + setProperty(DateTimeStamp.class, dateTimeStamp); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the journal + * entry was last modified (the {@link LastModified} property also holds + * this information). This journal entry object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @return the property that was created + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp setDateTimeStamp(Date dateTimeStamp) { + DateTimeStamp prop = (dateTimeStamp == null) ? null : new DateTimeStamp(dateTimeStamp); + setDateTimeStamp(prop); + return prop; + } + + /** + * Gets the level of sensitivity of the journal entry. If not specified, the + * data within the journal entry should be considered "public". + * @return the classification level or null if not set + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + */ + public Classification getClassification() { + return getProperty(Classification.class); + } + + /** + * Sets the level of sensitivity of the journal entry. If not specified, the + * data within the journal entry should be considered "public". + * @param classification the classification level or null to remove + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + */ + public void setClassification(Classification classification) { + setProperty(Classification.class, classification); + } + + /** + * Sets the level of sensitivity of the journal entry. If not specified, the + * data within the journal entry should be considered "public". + * @param classification the classification level (e.g. "CONFIDENTIAL") or + * null to remove + * @return the property that was created + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + */ + public Classification setClassification(String classification) { + Classification prop = (classification == null) ? null : new Classification(classification); + setClassification(prop); + return prop; + } + + /** + * Gets the date-time that the journal entry was initially created. + * @return the creation date-time or null if not set + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + */ + public Created getCreated() { + return getProperty(Created.class); + } + + /** + * Sets the date-time that the journal entry was initially created. + * @param created the creation date-time or null to remove + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + */ + public void setCreated(Created created) { + setProperty(Created.class, created); + } + + /** + * Sets the date-time that the journal entry was initially created. + * @param created the creation date-time or null to remove + * @return the property that was created + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + */ + public Created setCreated(Date created) { + Created prop = (created == null) ? null : new Created(created); + setCreated(prop); + return prop; + } + + /** + * Gets the date that the journal entry starts. + * @return the start date or null if not set + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart getDateStart() { + return getProperty(DateStart.class); + } + + /** + * Sets the date that the journal entry starts. + * @param dateStart the start date or null to remove + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public void setDateStart(DateStart dateStart) { + setProperty(DateStart.class, dateStart); + } + + /** + * Sets the date that the journal entry starts. + * @param dateStart the start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart setDateStart(Date dateStart) { + return setDateStart(dateStart, true); + } + + /** + * Sets the date that the journal entry starts. + * @param dateStart the start date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + */ + public DateStart setDateStart(Date dateStart, boolean hasTime) { + DateStart prop = (dateStart == null) ? null : new DateStart(dateStart, hasTime); + setDateStart(prop); + return prop; + } + + /** + * Gets the date-time that the journal entry was last changed. + * @return the last modified date or null if not set + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + */ + public LastModified getLastModified() { + return getProperty(LastModified.class); + } + + /** + * Sets the date-time that the journal entry was last changed. + * @param lastModified the last modified date or null to remove + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + */ + public void setLastModified(LastModified lastModified) { + setProperty(LastModified.class, lastModified); + } + + /** + * Sets the date-time that the journal entry was last changed. + * @param lastModified the last modified date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + */ + public LastModified setLastModified(Date lastModified) { + LastModified prop = (lastModified == null) ? null : new LastModified(lastModified); + setLastModified(prop); + return prop; + } + + /** + * Gets the organizer of the journal entry. + * @return the organizer or null if not set + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer getOrganizer() { + return getProperty(Organizer.class); + } + + /** + * Sets the organizer of the journal entry. + * @param organizer the organizer or null to remove + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public void setOrganizer(Organizer organizer) { + setProperty(Organizer.class, organizer); + } + + /** + * Sets the organizer of the journal entry. + * @param email the organizer's email address (e.g. "johndoe@example.com") + * or null to remove + * @return the property that was created + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer setOrganizer(String email) { + Organizer prop = (email == null) ? null : new Organizer(null, email); + setOrganizer(prop); + return prop; + } + + /** + * Gets the original value of the {@link DateStart} property if the event is + * recurring and has been modified. Used in conjunction with the {@link Uid} + * and {@link Sequence} properties to uniquely identify a recurrence + * instance. + * @return the recurrence ID or null if not set + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public RecurrenceId getRecurrenceId() { + return getProperty(RecurrenceId.class); + } + + /** + * Sets the original value of the {@link DateStart} property if the event is + * recurring and has been modified. Used in conjunction with the {@link Uid} + * and {@link Sequence} properties to uniquely identify a recurrence + * instance. + * @param recurrenceId the recurrence ID or null to remove + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public void setRecurrenceId(RecurrenceId recurrenceId) { + setProperty(RecurrenceId.class, recurrenceId); + } + + /** + * Sets the original value of the {@link DateStart} property if the journal + * entry is recurring and has been modified. Used in conjunction with the + * {@link Uid} and {@link Sequence} properties to uniquely identify a + * recurrence instance. + * @param originalStartDate the original start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public RecurrenceId setRecurrenceId(Date originalStartDate) { + RecurrenceId prop = (originalStartDate == null) ? null : new RecurrenceId(originalStartDate); + setRecurrenceId(prop); + return prop; + } + + /** + * Gets the revision number of the journal entry. The organizer can + * increment this number every time he or she makes a significant change. + * @return the sequence number + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + */ + public Sequence getSequence() { + return getProperty(Sequence.class); + } + + /** + * Sets the revision number of the journal entry. The organizer can + * increment this number every time he or she makes a significant change. + * @param sequence the sequence number + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + */ + public void setSequence(Sequence sequence) { + setProperty(Sequence.class, sequence); + } + + /** + * Sets the revision number of the journal entry. The organizer can + * increment this number every time he or she makes a significant change. + * @param sequence the sequence number + * @return the property that was created + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + */ + public Sequence setSequence(Integer sequence) { + Sequence prop = (sequence == null) ? null : new Sequence(sequence); + setSequence(prop); + return prop; + } + + /** + * Increments the revision number of the journal entry. The organizer can + * increment this number every time he or she makes a significant change. + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + */ + public void incrementSequence() { + Sequence sequence = getSequence(); + if (sequence == null) { + setSequence(1); + } else { + sequence.increment(); + } + } + + /** + * Gets the status of the journal entry. + * @return the status or null if not set + * @see RFC 5545 + * p.92-3 + * @see RFC 2445 + * p.88-9 + */ + public Status getStatus() { + return getProperty(Status.class); + } + + /** + *

+ * Sets the status of the journal entry. + *

+ *

+ * Valid journal status codes are: + *

+ *
    + *
  • DRAFT
  • + *
  • FINAL
  • + *
  • CANCELLED
  • + *
+ * @param status the status or null to remove + * @see RFC 5545 + * p.92-3 + * @see RFC 2445 + * p.88-9 + */ + public void setStatus(Status status) { + setProperty(Status.class, status); + } + + /** + * Gets the summary of the journal entry. + * @return the summary or null if not set + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + */ + public Summary getSummary() { + return getProperty(Summary.class); + } + + /** + * Sets the summary of the journal entry. + * @param summary the summary or null to remove + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + */ + public void setSummary(Summary summary) { + setProperty(Summary.class, summary); + } + + /** + * Sets the summary of the journal entry. + * @param summary the summary or null to remove + * @return the property that was created + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + */ + public Summary setSummary(String summary) { + Summary prop = (summary == null) ? null : new Summary(summary); + setSummary(prop); + return prop; + } + + /** + * Gets a URL to a resource that contains additional information about the + * journal entry. + * @return the URL or null if not set + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + */ + public Url getUrl() { + return getProperty(Url.class); + } + + /** + * Sets a URL to a resource that contains additional information about the + * journal entry. + * @param url the URL or null to remove + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + */ + public void setUrl(Url url) { + setProperty(Url.class, url); + } + + /** + * Sets a URL to a resource that contains additional information about the + * journal entry. + * @param url the URL (e.g. "http://example.com/resource.ics") or null to + * remove + * @return the property that was created + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + */ + public Url setUrl(String url) { + Url prop = (url == null) ? null : new Url(url); + setUrl(prop); + return prop; + } + + /** + * Gets how often the journal entry repeats. + * @return the recurrence rule or null if not set + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + */ + public RecurrenceRule getRecurrenceRule() { + return getProperty(RecurrenceRule.class); + } + + /** + * Sets how often the journal entry repeats. + * @param recur the recurrence rule or null to remove + * @return the property that was created + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + */ + public RecurrenceRule setRecurrenceRule(Recurrence recur) { + RecurrenceRule prop = (recur == null) ? null : new RecurrenceRule(recur); + setRecurrenceRule(prop); + return prop; + } + + /** + * Sets how often the journal entry repeats. + * @param recurrenceRule the recurrence rule or null to remove + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + */ + public void setRecurrenceRule(RecurrenceRule recurrenceRule) { + setProperty(RecurrenceRule.class, recurrenceRule); + } + + /** + * Gets any attachments that are associated with the journal entry. + * @return the attachments (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + */ + public List getAttachments() { + return getProperties(Attachment.class); + } + + /** + * Adds an attachment to the journal entry. + * @param attachment the attachment to add + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + */ + public void addAttachment(Attachment attachment) { + addProperty(attachment); + } + + /** + * Gets the people who are involved in the journal entry. + * @return the attendees (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public List getAttendees() { + return getProperties(Attendee.class); + } + + /** + * Adds a person who is involved in the journal entry. + * @param attendee the attendee + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public void addAttendee(Attendee attendee) { + addProperty(attendee); + } + + /** + * Adds a person who is involved in the journal entry. + * @param email the attendee's email address + * @return the property that was created + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + */ + public Attendee addAttendee(String email) { + Attendee prop = new Attendee(null, email); + addAttendee(prop); + return prop; + } + + /** + * Gets a list of "tags" or "keywords" that describe the journal entry. + * @return the categories (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + */ + public List getCategories() { + return getProperties(Categories.class); + } + + /** + * Adds a list of "tags" or "keywords" that describe the journal entry. Note + * that a single property can hold multiple keywords. + * @param categories the categories to add + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + */ + public void addCategories(Categories categories) { + addProperty(categories); + } + + /** + * Adds a list of "tags" or "keywords" that describe the journal entry. + * @param categories the categories to add + * @return the property that was created + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + */ + public Categories addCategories(String... categories) { + Categories prop = new Categories(categories); + addCategories(prop); + return prop; + } + + /** + * Adds a list of "tags" or "keywords" that describe the journal entry. + * @param categories the categories to add + * @return the property that was created + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + */ + public Categories addCategories(List categories) { + Categories prop = new Categories(categories); + addCategories(prop); + return prop; + } + + /** + * Gets the comments attached to the journal entry. + * @return the comments (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public List getComments() { + return getProperties(Comment.class); + } + + /** + * Adds a comment to the journal entry. + * @param comment the comment to add + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public void addComment(Comment comment) { + addProperty(comment); + } + + /** + * Adds a comment to the journal entry. + * @param comment the comment to add + * @return the property that was created + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public Comment addComment(String comment) { + Comment prop = new Comment(comment); + addComment(prop); + return prop; + } + + /** + * Gets the contacts associated with the journal entry. + * @return the contacts (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public List getContacts() { + return getProperties(Contact.class); + } + + /** + * Adds a contact to the journal entry. + * @param contact the contact + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public void addContact(Contact contact) { + addProperty(contact); + } + + /** + * Adds a contact to the journal entry. + * @param contact the contact (e.g. "ACME Co - (123) 555-1234") + * @return the property that was created + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public Contact addContact(String contact) { + Contact prop = new Contact(contact); + addContact(prop); + return prop; + } + + /** + * Gets the detailed descriptions to the journal entry. The descriptions + * should be a more detailed version of the one provided by the + * {@link Summary} property. + * @return the descriptions (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + */ + public List getDescriptions() { + return getProperties(Description.class); + } + + /** + * Adds a detailed description to the journal entry. The description should + * be a more detailed version of the one provided by the {@link Summary} + * property. + * @param description the description + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + */ + public void addDescription(Description description) { + addProperty(description); + } + + /** + * Adds a detailed description to the journal entry. The description should + * be a more detailed version of the one provided by the {@link Summary} + * property. + * @param description the description + * @return the property that was created + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + */ + public Description addDescription(String description) { + Description prop = new Description(description); + addDescription(prop); + return prop; + } + + /** + * Gets the list of exceptions to the recurrence rule defined in the journal + * entry (if one is defined). + * @return the list of exceptions (any changes made this list will affect + * the parent component object and vice versa) + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + */ + public List getExceptionDates() { + return getProperties(ExceptionDates.class); + } + + /** + * Adds a list of exceptions to the recurrence rule defined in the journal + * entry (if one is defined). Note that this property can contain multiple + * dates. + * @param exceptionDates the list of exceptions + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + */ + public void addExceptionDates(ExceptionDates exceptionDates) { + addProperty(exceptionDates); + } + + /** + * Gets the components that the journal entry is related to. + * @return the relationships (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + */ + public List getRelatedTo() { + return getProperties(RelatedTo.class); + } + + /** + * Adds a component that the journal entry is related to. + * @param relatedTo the relationship + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + */ + public void addRelatedTo(RelatedTo relatedTo) { + //TODO create a method that accepts a component and make the RelatedTo property invisible to the user + //@formatter:off + /* + * addRelation(RelationshipType relType, ICalComponent component) { + * RelatedTo prop = new RelatedTo(component.getUid().getValue()); + * prop.setRelationshipType(relType); + * addProperty(prop); + * } + */ + //@formatter:on + addProperty(relatedTo); + } + + /** + * Adds a component that the journal entry is related to. + * @param uid the UID of the other component + * @return the property that was created + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + */ + public RelatedTo addRelatedTo(String uid) { + RelatedTo prop = new RelatedTo(uid); + addRelatedTo(prop); + return prop; + } + + /** + * Gets the list of dates/periods that help define the recurrence rule of + * this journal entry (if one is defined). + * @return the recurrence dates (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + */ + public List getRecurrenceDates() { + return getProperties(RecurrenceDates.class); + } + + /** + * Adds a list of dates/periods that help define the recurrence rule of this + * journal entry (if one is defined). + * @param recurrenceDates the recurrence dates + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + */ + public void addRecurrenceDates(RecurrenceDates recurrenceDates) { + addProperty(recurrenceDates); + } + + /** + * Gets the response to a scheduling request. + * @return the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public RequestStatus getRequestStatus() { + return getProperty(RequestStatus.class); + } + + /** + * Sets the response to a scheduling request. + * @param requestStatus the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public void setRequestStatus(RequestStatus requestStatus) { + setProperty(RequestStatus.class, requestStatus); + } + + /** + *

+ * Gets the exceptions for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @return the exception rules (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 2445 + * p.114-15 + */ + public List getExceptionRules() { + return getProperties(ExceptionRule.class); + } + + /** + *

+ * Adds an exception for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @param recur the exception rule to add + * @return the property that was created + * @see RFC 2445 + * p.114-15 + */ + public ExceptionRule addExceptionRule(Recurrence recur) { + ExceptionRule prop = new ExceptionRule(recur); + addExceptionRule(prop); + return prop; + } + + /** + *

+ * Adds an exception for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @param exceptionRule the exception rule to add + * @see RFC 2445 + * p.114-15 + */ + public void addExceptionRule(ExceptionRule exceptionRule) { + addProperty(exceptionRule); + } + + /** + * Gets the color that clients may use when displaying the journal entry + * (for example, a background color). + * @return the property or null if not set + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color getColor() { + return getProperty(Color.class); + } + + /** + * Sets the color that clients may use when displaying the journal entry + * (for example, a background color). + * @param color the property or null to remove + * @see draft-ietf-calext-extensions-01 + * p.79 + */ + public void setColor(Color color) { + setProperty(Color.class, color); + } + + /** + * Sets the color that clients may use when displaying the journal entry + * (for example, a background color). + * @param color the color name (case insensitive) or null to remove. + * Acceptable values are defined in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color setColor(String color) { + Color prop = (color == null) ? null : new Color(color); + setColor(prop); + return prop; + } + + /** + * Gets the images that are associated with the journal entry. + * @return the images (any changes made this list will affect the parent + * component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public List getImages() { + return getProperties(Image.class); + } + + /** + * Adds an image that is associated with the journal entry. + * @param image the property to add + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public void addImage(Image image) { + addProperty(image); + } + + /** + *

+ * Creates an iterator that computes the dates defined by the + * {@link RecurrenceRule} and {@link RecurrenceDates} properties (if + * present), and excludes those dates which are defined by the + * {@link ExceptionRule} and {@link ExceptionDates} properties (if present). + *

+ *

+ * In order for {@link RecurrenceRule} and {@link ExceptionRule} properties + * to be included in this iterator, a {@link DateStart} property must be + * defined. + *

+ *

+ * {@link Period} values in {@link RecurrenceDates} properties are not + * supported and are ignored. + *

+ * @param timezone the timezone to iterate in. This is needed in order to + * adjust for when the iterator passes over a daylight savings boundary. + * This parameter is ignored if the start date does not have a time + * component. + * @return the iterator + */ + public DateIterator getDateIterator(TimeZone timezone) { + return Google2445Utils.getDateIterator(this, timezone); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version == ICalVersion.V1_0) { + warnings.add(new ValidationWarning(48, version)); + } + + checkRequiredCardinality(warnings, Uid.class, DateTimeStamp.class); + checkOptionalCardinality(warnings, Classification.class, Created.class, DateStart.class, LastModified.class, Organizer.class, RecurrenceId.class, Sequence.class, Status.class, Summary.class, Url.class, Color.class); + checkStatus(warnings, Status.draft(), Status.final_(), Status.cancelled()); + + //DTSTART and RECURRENCE-ID must have the same data type + ICalDate recurrenceId = getValue(getRecurrenceId()); + ICalDate dateStart = getValue(getDateStart()); + if (recurrenceId != null && dateStart != null && dateStart.hasTime() != recurrenceId.hasTime()) { + warnings.add(new ValidationWarning(19)); + } + + //BYHOUR, BYMINUTE, and BYSECOND cannot be specified in RRULE if DTSTART's data type is "date" + //RFC 5545 p. 167 + Recurrence rrule = getValue(getRecurrenceRule()); + if (dateStart != null && rrule != null) { + if (!dateStart.hasTime() && (!rrule.getByHour().isEmpty() || !rrule.getByMinute().isEmpty() || !rrule.getBySecond().isEmpty())) { + warnings.add(new ValidationWarning(5)); + } + } + + //there *should* be only 1 instance of RRULE + //RFC 5545 p. 167 + if (getProperties(RecurrenceRule.class).size() > 1) { + warnings.add(new ValidationWarning(6)); + } + } + + @Override + public VJournal copy() { + return new VJournal(this); + } +} diff --git a/app/src/main/java/biweekly/component/VTimezone.java b/app/src/main/java/biweekly/component/VTimezone.java new file mode 100644 index 0000000000..4d51c85721 --- /dev/null +++ b/app/src/main/java/biweekly/component/VTimezone.java @@ -0,0 +1,282 @@ +package biweekly.component; + +import java.util.Date; +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.property.LastModified; +import biweekly.property.TimezoneId; +import biweekly.property.TimezoneUrl; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a timezone's UTC offsets throughout the year. + *

+ * + *

+ * Examples: + *

+ * + *
+ * VTimezone timezone = new VTimezone("Eastern Standard Time");
+ * 
+ * StandardTime standard = new StandardTime();
+ * DateTimeComponents componentsStandard = new DateTimeComponents(1998, 10, 25, 2, 0, 0, false);
+ * standard.setDateStart(componentsStandard);
+ * standard.setTimezoneOffsetFrom(-4, 0);
+ * standard.setTimezoneOffsetTo(-5, 0);
+ * timezone.addStandardTime(standard);
+ * 
+ * DaylightSavingsTime daylight = new DaylightSavingsTime();
+ * DateTimeComponents componentsDaylight = new DateTimeComponents(1999, 4, 4, 2, 0, 0, false);
+ * daylight.setDateStart(componentsDaylight);
+ * daylight.setTimezoneOffsetFrom(-5, 0);
+ * daylight.setTimezoneOffsetTo(-4, 0);
+ * timezone.addDaylightSavingsTime(daylight);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 p.60-7 + */ +/* + * Note: References to the vCal 1.0 spec are omitted from the property + * getter/setter method Javadocs because vCal does not use the VTIMEZONE + * component. + */ +public class VTimezone extends ICalComponent { + /** + * Creates a new timezone component. + * @param identifier a unique identifier for this timezone (allows it to be + * referenced by date-time properties that support timezones). + */ + public VTimezone(String identifier) { + setTimezoneId(identifier); + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public VTimezone(VTimezone original) { + super(original); + } + + /** + * Gets the ID for this timezone. This is a required property. + * @return the timezone ID or null if not set + * @see RFC 5545 + * p.102-3 + * @see RFC 2445 + * p.97-8 + */ + public TimezoneId getTimezoneId() { + return getProperty(TimezoneId.class); + } + + /** + * Sets an ID for this timezone. This is a required property. + * @param timezoneId the timezone ID or null to remove + * @see RFC 5545 + * p.102-3 + * @see RFC 2445 + * p.97-8 + */ + public void setTimezoneId(TimezoneId timezoneId) { + setProperty(TimezoneId.class, timezoneId); + } + + /** + * Sets an ID for this timezone. This is a required property. + * @param timezoneId the timezone ID or null to remove + * @return the property that was created + * @see RFC 5545 + * p.102-3 + * @see RFC 2445 + * p.97-8 + */ + public TimezoneId setTimezoneId(String timezoneId) { + TimezoneId prop = (timezoneId == null) ? null : new TimezoneId(timezoneId); + setTimezoneId(prop); + return prop; + } + + /** + * Gets the date-time that the timezone data was last changed. + * @return the last modified date or null if not set + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + */ + public LastModified getLastModified() { + return getProperty(LastModified.class); + } + + /** + * Sets the date-time that the timezone data was last changed. + * @param lastModified the last modified date or null to remove + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + */ + public void setLastModified(LastModified lastModified) { + setProperty(LastModified.class, lastModified); + } + + /** + * Sets the date-time that the timezone data was last changed. + * @param lastModified the last modified date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + */ + public LastModified setLastModified(Date lastModified) { + LastModified prop = (lastModified == null) ? null : new LastModified(lastModified); + setLastModified(prop); + return prop; + } + + /** + * Gets the timezone URL, which points to an iCalendar object that contains + * further information on the timezone. + * @return the URL or null if not set + * @see RFC 5545 + * p.106 + * @see RFC 2445 + * p.101 + */ + public TimezoneUrl getTimezoneUrl() { + return getProperty(TimezoneUrl.class); + } + + /** + * Sets the timezone URL, which points to an iCalendar object that contains + * further information on the timezone. + * @param url the URL or null to remove + * @see RFC 5545 + * p.106 + * @see RFC 2445 + * p.101 + */ + public void setTimezoneUrl(TimezoneUrl url) { + setProperty(TimezoneUrl.class, url); + } + + /** + * Sets the timezone URL, which points to an iCalendar object that contains + * further information on the timezone. + * @param url the timezone URL (e.g. + * "http://example.com/America-New_York.ics") or null to remove + * @return the property that was created + * @see RFC 5545 + * p.106 + * @see RFC 2445 + * p.101 + */ + public TimezoneUrl setTimezoneUrl(String url) { + TimezoneUrl prop = (url == null) ? null : new TimezoneUrl(url); + setTimezoneUrl(prop); + return prop; + } + + /** + * Gets the timezone's "standard" observance time ranges. + * @return the "standard" observance time ranges (any changes made this list + * will affect the parent component object and vice versa) + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 + * p.60-7 + */ + public List getStandardTimes() { + return getComponents(StandardTime.class); + } + + /** + * Adds a "standard" observance time range. + * @param standardTime the "standard" observance time + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 + * p.60-7 + */ + public void addStandardTime(StandardTime standardTime) { + addComponent(standardTime); + } + + /** + * Gets the timezone's "daylight savings" observance time ranges. + * @return the "daylight savings" observance time ranges (any changes made + * this list will affect the parent component object and vice versa) + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 + * p.60-7 + */ + public List getDaylightSavingsTime() { + return getComponents(DaylightSavingsTime.class); + } + + /** + * Adds a "daylight savings" observance time range. + * @param daylightSavingsTime the "daylight savings" observance time + * @see RFC 5545 + * p.62-71 + * @see RFC 2445 + * p.60-7 + */ + public void addDaylightSavingsTime(DaylightSavingsTime daylightSavingsTime) { + addComponent(daylightSavingsTime); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version == ICalVersion.V1_0) { + warnings.add(new ValidationWarning(48, version)); + } + + checkRequiredCardinality(warnings, TimezoneId.class); + checkOptionalCardinality(warnings, LastModified.class, TimezoneUrl.class); + + //STANDARD or DAYLIGHT must be defined + if (getStandardTimes().isEmpty() && getDaylightSavingsTime().isEmpty()) { + warnings.add(new ValidationWarning(21)); + } + } + + @Override + public VTimezone copy() { + return new VTimezone(this); + } +} diff --git a/app/src/main/java/biweekly/component/VTodo.java b/app/src/main/java/biweekly/component/VTodo.java new file mode 100644 index 0000000000..87ee266259 --- /dev/null +++ b/app/src/main/java/biweekly/component/VTodo.java @@ -0,0 +1,1754 @@ +package biweekly.component; + +import static biweekly.property.ValuedProperty.getValue; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.property.Attachment; +import biweekly.property.Attendee; +import biweekly.property.Categories; +import biweekly.property.Classification; +import biweekly.property.Color; +import biweekly.property.Comment; +import biweekly.property.Completed; +import biweekly.property.Conference; +import biweekly.property.Contact; +import biweekly.property.Created; +import biweekly.property.DateDue; +import biweekly.property.DateStart; +import biweekly.property.DateTimeStamp; +import biweekly.property.Description; +import biweekly.property.DurationProperty; +import biweekly.property.ExceptionDates; +import biweekly.property.ExceptionRule; +import biweekly.property.Geo; +import biweekly.property.Image; +import biweekly.property.LastModified; +import biweekly.property.Location; +import biweekly.property.Method; +import biweekly.property.Organizer; +import biweekly.property.PercentComplete; +import biweekly.property.Priority; +import biweekly.property.RecurrenceDates; +import biweekly.property.RecurrenceId; +import biweekly.property.RecurrenceRule; +import biweekly.property.RelatedTo; +import biweekly.property.RequestStatus; +import biweekly.property.Resources; +import biweekly.property.Sequence; +import biweekly.property.Status; +import biweekly.property.Summary; +import biweekly.property.Uid; +import biweekly.property.Url; +import biweekly.util.Duration; +import biweekly.util.Google2445Utils; +import biweekly.util.ICalDate; +import biweekly.util.Period; +import biweekly.util.Recurrence; +import biweekly.util.com.google.ical.compat.javautil.DateIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a task or assignment that needs to be completed at some point in the + * future. + *

+ *

+ * Examples: + *

+ * + *
+ * VTodo todo = new VTodo();
+ * todo.setSummary("Complete report");
+ * Date due = ...
+ * todo.setDateDue(due);
+ * todo.setStatus(Status.confirmed());
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.55-7 + * @see RFC 2445 p.55-6 + * @see vCal 1.0 p.14 + */ +public class VTodo extends ICalComponent { + /** + *

+ * Creates a new to-do task. + *

+ *

+ * The following properties are added to the component when it is created: + *

+ *
    + *
  • {@link Uid}: Set to a UUID.
  • + *
  • {@link DateTimeStamp}: Set to the current time.
  • + *
+ */ + public VTodo() { + setUid(Uid.random()); + setDateTimeStamp(new Date()); + } + + /** + * Copy constructor. + * @param original the component to make a copy of + */ + public VTodo(VTodo original) { + super(original); + } + + /** + * Gets the unique identifier for this to-do task. This component object + * comes populated with a UID on creation. This is a required + * property. + * @return the UID or null if not set + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + */ + public Uid getUid() { + return getProperty(Uid.class); + } + + /** + * Sets the unique identifier for this to-do task. This component object + * comes populated with a UID on creation. This is a required + * property. + * @param uid the UID or null to remove + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + */ + public void setUid(Uid uid) { + setProperty(Uid.class, uid); + } + + /** + * Sets the unique identifier for this to-do task. This component object + * comes populated with a UID on creation. This is a required + * property. + * @param uid the UID or null to remove + * @return the property that was created + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + */ + public Uid setUid(String uid) { + Uid prop = (uid == null) ? null : new Uid(uid); + setUid(prop); + return prop; + } + + /** + * Gets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the to-do task + * was last modified (the {@link LastModified} property also holds this + * information). This to-do object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @return the date time stamp or null if not set + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp getDateTimeStamp() { + return getProperty(DateTimeStamp.class); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the to-do task + * was last modified (the {@link LastModified} property also holds this + * information). This to-do object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public void setDateTimeStamp(DateTimeStamp dateTimeStamp) { + setProperty(DateTimeStamp.class, dateTimeStamp); + } + + /** + * Sets either (a) the creation date of the iCalendar object (if the + * {@link Method} property is defined) or (b) the date that the to-do task + * was last modified (the {@link LastModified} property also holds this + * information). This to-do object comes populated with a + * {@link DateTimeStamp} property that is set to the current time. This is a + * required property. + * @param dateTimeStamp the date time stamp or null to remove + * @return the property that was created + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ + public DateTimeStamp setDateTimeStamp(Date dateTimeStamp) { + DateTimeStamp prop = (dateTimeStamp == null) ? null : new DateTimeStamp(dateTimeStamp); + setDateTimeStamp(prop); + return prop; + } + + /** + * Gets the level of sensitivity of the to-do data. If not specified, the + * data within the to-do task should be considered "public". + * @return the classification level or null if not set + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ + public Classification getClassification() { + return getProperty(Classification.class); + } + + /** + * Sets the level of sensitivity of the to-do data. If not specified, the + * data within the to-do task should be considered "public". + * @param classification the classification level or null to remove + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ + public void setClassification(Classification classification) { + setProperty(Classification.class, classification); + } + + /** + * Sets the level of sensitivity of the to-do data. If not specified, the + * data within the to-do task should be considered "public". + * @param classification the classification level (e.g. "CONFIDENTIAL") or + * null to remove + * @return the property that was created + * @see RFC 5545 + * p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ + public Classification setClassification(String classification) { + Classification prop = (classification == null) ? null : new Classification(classification); + setClassification(prop); + return prop; + } + + /** + * Gets the date and time that the to-do task was completed. + * @return the completion date or null if not set + * @see RFC 5545 + * p.94-5 + * @see RFC 2445 + * p.90-1 + * @see vCal 1.0 p.29 + */ + public Completed getCompleted() { + return getProperty(Completed.class); + } + + /** + * Sets the date and time that the to-do task was completed. + * @param completed the completion date or null to remove + * @see RFC 5545 + * p.94-5 + * @see RFC 2445 + * p.90-1 + * @see vCal 1.0 p.29 + */ + public void setCompleted(Completed completed) { + setProperty(Completed.class, completed); + } + + /** + * Sets the date and time that the to-do task was completed. + * @param completed the completion date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.94-5 + * @see RFC 2445 + * p.90-1 + * @see vCal 1.0 p.29 + */ + public Completed setCompleted(Date completed) { + Completed prop = (completed == null) ? null : new Completed(completed); + setCompleted(prop); + return prop; + } + + /** + * Gets the date-time that the to-do task was initially created. + * @return the creation date-time or null if not set + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ + public Created getCreated() { + return getProperty(Created.class); + } + + /** + * Sets the date-time that the to-do task was initially created. + * @param created the creation date-time or null to remove + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ + public void setCreated(Created created) { + setProperty(Created.class, created); + } + + /** + * Sets the date-time that the to-do task was initially created. + * @param created the creation date-time or null to remove + * @return the property that was created + * @see RFC 5545 + * p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ + public Created setCreated(Date created) { + Created prop = (created == null) ? null : new Created(created); + setCreated(prop); + return prop; + } + + /** + * Gets a detailed description of the to-do task. The description should be + * more detailed than the one provided by the {@link Summary} property. + * @return the description or null if not set + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + * @see vCal 1.0 p.30 + */ + public Description getDescription() { + return getProperty(Description.class); + } + + /** + * Sets a detailed description of the to-do task. The description should be + * more detailed than the one provided by the {@link Summary} property. + * @param description the description or null to remove + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + * @see vCal 1.0 p.30 + */ + public void setDescription(Description description) { + setProperty(Description.class, description); + } + + /** + * Sets a detailed description of the to-do task. The description should be + * more detailed than the one provided by the {@link Summary} property. + * @param description the description or null to remove + * @return the property that was created + * @see RFC 5545 + * p.84-5 + * @see RFC 2445 + * p.81-2 + * @see vCal 1.0 p.30 + */ + public Description setDescription(String description) { + Description prop = (description == null) ? null : new Description(description); + setDescription(prop); + return prop; + } + + /** + * Gets the date that the to-do task starts. + * @return the start date or null if not set + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public DateStart getDateStart() { + return getProperty(DateStart.class); + } + + /** + * Sets the date that the to-do task starts. + * @param dateStart the start date or null to remove + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public void setDateStart(DateStart dateStart) { + setProperty(DateStart.class, dateStart); + } + + /** + * Sets the date that the to-do task starts. + * @param dateStart the start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public DateStart setDateStart(Date dateStart) { + return setDateStart(dateStart, true); + } + + /** + * Sets the date that the to-do task starts. + * @param dateStart the start date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.97-8 + * @see RFC 2445 + * p.93-4 + * @see vCal 1.0 p.35 + */ + public DateStart setDateStart(Date dateStart, boolean hasTime) { + DateStart prop = (dateStart == null) ? null : new DateStart(dateStart, hasTime); + setDateStart(prop); + return prop; + } + + /** + * Gets a set of geographical coordinates. + * @return the geographical coordinates or null if not set + * @see RFC 5545 + * p.85-7 + * @see RFC 2445 + * p.82-3 + */ + //Note: vCal 1.0 spec is omitted because GEO is not used in vCal 1.0 to-dos + public Geo getGeo() { + return getProperty(Geo.class); + } + + /** + * Sets a set of geographical coordinates. + * @param geo the geographical coordinates or null to remove + * @see RFC 5545 + * p.85-7 + * @see RFC 2445 + * p.82-3 + */ + //Note: vCal 1.0 spec is omitted because GEO is not used in vCal 1.0 to-dos + public void setGeo(Geo geo) { + setProperty(Geo.class, geo); + } + + /** + * Gets the date-time that the to-do task was last changed. + * @return the last modified date or null if not set + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + * @see vCal 1.0 p.31 + */ + public LastModified getLastModified() { + return getProperty(LastModified.class); + } + + /** + * Sets the date-time that the to-do task was last changed. + * @param lastModified the last modified date or null to remove + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + * @see vCal 1.0 p.31 + */ + public void setLastModified(LastModified lastModified) { + setProperty(LastModified.class, lastModified); + } + + /** + * Sets the date-time that the to-do task was last changed. + * @param lastModified the last modified date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.138 + * @see RFC 2445 + * p.131 + * @see vCal 1.0 p.31 + */ + public LastModified setLastModified(Date lastModified) { + LastModified prop = (lastModified == null) ? null : new LastModified(lastModified); + setLastModified(prop); + return prop; + } + + /** + * Gets the physical location of the to-do task. + * @return the location or null if not set + * @see RFC 5545 + * p.87-8 + * @see RFC 2445 + * p.84 + * @see vCal 1.0 p.32 + */ + public Location getLocation() { + return getProperty(Location.class); + } + + /** + * Sets the physical location of the to-do task. + * @param location the location or null to remove + * @see RFC 5545 + * p.87-8 + * @see RFC 2445 + * p.84 + * @see vCal 1.0 p.32 + */ + public void setLocation(Location location) { + setProperty(Location.class, location); + } + + /** + * Sets the physical location of the to-do task. + * @param location the location (e.g. "Room 101") or null to remove + * @return the property that was created + * @see RFC 5545 + * p.87-8 + * @see RFC 2445 + * p.84 + * @see vCal 1.0 p.32 + */ + public Location setLocation(String location) { + Location prop = (location == null) ? null : new Location(location); + setLocation(prop); + return prop; + } + + /** + * Gets the organizer of the to-do task. + * @return the organizer or null if not set + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer getOrganizer() { + return getProperty(Organizer.class); + } + + /** + * Sets the organizer of the to-do task. + * @param organizer the organizer or null to remove + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public void setOrganizer(Organizer organizer) { + setProperty(Organizer.class, organizer); + } + + /** + * Sets the organizer of the to-do task. + * @param email the organizer's email address (e.g. "johndoe@example.com") + * or null to remove + * @return the property that was created + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ + public Organizer setOrganizer(String email) { + Organizer prop = (email == null) ? null : new Organizer(null, email); + setOrganizer(prop); + return prop; + } + + /** + * Gets the amount that the to-do task has been completed. + * @return the percent complete or null if not set + * @see RFC 5545 + * p.88-9 + * @see RFC 2445 + * p.85 + */ + public PercentComplete getPercentComplete() { + return getProperty(PercentComplete.class); + } + + /** + * Sets the amount that the to-do task has been completed. + * @param percentComplete the percent complete or null to remove + * @see RFC 5545 + * p.88-9 + * @see RFC 2445 + * p.85 + */ + public void setPercentComplete(PercentComplete percentComplete) { + setProperty(PercentComplete.class, percentComplete); + } + + /** + * Sets the amount that the to-do task has been completed. + * @param percent the percent complete (e.g. "50" for 50%) or null to remove + * @return the property that was created + * @see RFC 5545 + * p.88-9 + * @see RFC 2445 + * p.85 + */ + public PercentComplete setPercentComplete(Integer percent) { + PercentComplete prop = (percent == null) ? null : new PercentComplete(percent); + setPercentComplete(prop); + return prop; + } + + /** + * Gets the priority of the to-do task. + * @return the priority or null if not set + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 + * p.85-7 + * @see vCal 1.0 p.33 + */ + public Priority getPriority() { + return getProperty(Priority.class); + } + + /** + * Sets the priority of the to-do task. + * @param priority the priority or null to remove + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 + * p.85-7 + * @see vCal 1.0 p.33 + */ + public void setPriority(Priority priority) { + setProperty(Priority.class, priority); + } + + /** + * Sets the priority of the to-do task. + * @param priority the priority ("0" is undefined, "1" is the highest, "9" + * is the lowest) or null to remove + * @return the property that was created + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 + * p.85-7 + * @see vCal 1.0 p.33 + */ + public Priority setPriority(Integer priority) { + Priority prop = (priority == null) ? null : new Priority(priority); + setPriority(prop); + return prop; + } + + /** + * Gets the original value of the {@link DateStart} property if the to-do + * task is recurring and has been modified. Used in conjunction with the + * {@link Uid} and {@link Sequence} properties to uniquely identify a + * recurrence instance. + * @return the recurrence ID or null if not set + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public RecurrenceId getRecurrenceId() { + return getProperty(RecurrenceId.class); + } + + /** + * Sets the original value of the {@link DateStart} property if the to-do + * task is recurring and has been modified. Used in conjunction with the + * {@link Uid} and {@link Sequence} properties to uniquely identify a + * recurrence instance. + * @param recurrenceId the recurrence ID or null to remove + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public void setRecurrenceId(RecurrenceId recurrenceId) { + setProperty(RecurrenceId.class, recurrenceId); + } + + /** + * Sets the original value of the {@link DateStart} property if the to-do + * task is recurring and has been modified. Used in conjunction with the + * {@link Uid} and {@link Sequence} properties to uniquely identify a + * recurrence instance. + * @param originalStartDate the original start date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ + public RecurrenceId setRecurrenceId(Date originalStartDate) { + RecurrenceId prop = (originalStartDate == null) ? null : new RecurrenceId(originalStartDate); + setRecurrenceId(prop); + return prop; + } + + /** + * Gets the revision number of the to-do task. The organizer can increment + * this number every time he or she makes a significant change. + * @return the sequence number + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public Sequence getSequence() { + return getProperty(Sequence.class); + } + + /** + * Sets the revision number of the to-do task. The organizer can increment + * this number every time he or she makes a significant change. + * @param sequence the sequence number + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public void setSequence(Sequence sequence) { + setProperty(Sequence.class, sequence); + } + + /** + * Sets the revision number of the to-do task. The organizer can increment + * this number every time he or she makes a significant change. + * @param sequence the sequence number + * @return the property that was created + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public Sequence setSequence(Integer sequence) { + Sequence prop = (sequence == null) ? null : new Sequence(sequence); + setSequence(prop); + return prop; + } + + /** + * Increments the revision number of the to-do task. The organizer can + * increment this number every time he or she makes a significant change. + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ + public void incrementSequence() { + Sequence sequence = getSequence(); + if (sequence == null) { + setSequence(1); + } else { + sequence.increment(); + } + } + + /** + * Gets the status of the to-do task. + * @return the status or null if not set + * @see RFC 5545 + * p.92-3 + * @see RFC 2445 + * p.88-9 + * @see vCal 1.0 p.35-6 + */ + public Status getStatus() { + return getProperty(Status.class); + } + + /** + *

+ * Sets the status of the to-do task. + *

+ *

+ * Valid status codes are: + *

+ *
    + *
  • NEEDS-ACTION
  • + *
  • COMPLETED
  • + *
  • IN-PROGRESS
  • + *
  • CANCELLED
  • + *
+ * @param status the status or null to remove + * @see RFC 5545 + * p.92-3 + * @see RFC 2445 + * p.88-9 + * @see vCal 1.0 p.35-6 + */ + public void setStatus(Status status) { + setProperty(Status.class, status); + } + + /** + * Gets the summary of the to-do task. + * @return the summary or null if not set + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ + public Summary getSummary() { + return getProperty(Summary.class); + } + + /** + * Sets the summary of the to-do task. + * @param summary the summary or null to remove + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ + public void setSummary(Summary summary) { + setProperty(Summary.class, summary); + } + + /** + * Sets the summary of the to-do task. + * @param summary the summary or null to remove + * @return the property that was created + * @see RFC 5545 + * p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ + public Summary setSummary(String summary) { + Summary prop = (summary == null) ? null : new Summary(summary); + setSummary(prop); + return prop; + } + + /** + * Gets a URL to a resource that contains additional information about the + * to-do task. + * @return the URL or null if not set + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + */ + public Url getUrl() { + return getProperty(Url.class); + } + + /** + * Sets a URL to a resource that contains additional information about the + * to-do task. + * @param url the URL or null to remove + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + */ + public void setUrl(Url url) { + setProperty(Url.class, url); + } + + /** + * Sets a URL to a resource that contains additional information about the + * to-do task. + * @param url the URL (e.g. "http://example.com/resource.ics") or null to + * remove + * @return the property that was created + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + */ + public Url setUrl(String url) { + Url prop = (url == null) ? null : new Url(url); + setUrl(prop); + return prop; + } + + /** + * Gets how often the to-do task repeats. + * @return the recurrence rule or null if not set + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ + public RecurrenceRule getRecurrenceRule() { + return getProperty(RecurrenceRule.class); + } + + /** + * Sets how often the to-do task repeats. + * @param recur the recurrence rule or null to remove + * @return the property that was created + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ + public RecurrenceRule setRecurrenceRule(Recurrence recur) { + RecurrenceRule prop = (recur == null) ? null : new RecurrenceRule(recur); + setRecurrenceRule(prop); + return prop; + } + + /** + * Sets how often the to-do task repeats. + * @param recurrenceRule the recurrence rule or null to remove + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ + public void setRecurrenceRule(RecurrenceRule recurrenceRule) { + setProperty(RecurrenceRule.class, recurrenceRule); + } + + /** + * Gets the date that a to-do task is due by. + * @return the due date or null if not set + * @see RFC 5545 + * p.96-7 + * @see RFC 2445 + * p.92-3 + * @see vCal 1.0 p.30 + */ + public DateDue getDateDue() { + return getProperty(DateDue.class); + } + + /** + * Sets the date that a to-do task is due by. This must NOT be set if a + * {@link DurationProperty} is defined. + * @param dateDue the due date or null to remove + * @see RFC 5545 + * p.96-7 + * @see RFC 2445 + * p.92-3 + * @see vCal 1.0 p.30 + */ + public void setDateDue(DateDue dateDue) { + setProperty(DateDue.class, dateDue); + } + + /** + * Sets the date that a to-do task is due by. This must NOT be set if a + * {@link DurationProperty} is defined. + * @param dateDue the due date or null to remove + * @return the property that was created + * @see RFC 5545 + * p.96-7 + * @see RFC 2445 + * p.92-3 + * @see vCal 1.0 p.30 + */ + public DateDue setDateDue(Date dateDue) { + return setDateDue(dateDue, true); + } + + /** + * Sets the date that a to-do task is due by. This must NOT be set if a + * {@link DurationProperty} is defined. + * @param dateDue the due date or null to remove + * @param hasTime true if the date has a time component, false if it is + * strictly a date (if false, the given Date object should be created by a + * {@link java.util.Calendar Calendar} object that uses the JVM's default + * timezone) + * @return the property that was created + * @see RFC 5545 + * p.96-7 + * @see RFC 2445 + * p.92-3 + * @see vCal 1.0 p.30 + */ + public DateDue setDateDue(Date dateDue, boolean hasTime) { + DateDue prop = (dateDue == null) ? null : new DateDue(dateDue, hasTime); + setDateDue(prop); + return prop; + } + + /** + * Gets the duration of the to-do task. + * @return the duration or null if not set + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public DurationProperty getDuration() { + return getProperty(DurationProperty.class); + } + + /** + * Sets the duration of the to-do task. This must NOT be set if a + * {@link DateDue} is defined. + * @param duration the duration or null to remove + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public void setDuration(DurationProperty duration) { + setProperty(DurationProperty.class, duration); + } + + /** + * Sets the duration of the to-do task. This must NOT be set if a + * {@link DateDue} is defined. + * @param duration the duration or null to remove + * @return the property that was created + * @see RFC 5545 + * p.99 + * @see RFC 2445 + * p.94-5 + */ + public DurationProperty setDuration(Duration duration) { + DurationProperty prop = (duration == null) ? null : new DurationProperty(duration); + setDuration(prop); + return prop; + } + + /** + * Gets any attachments that are associated with the to-do task. + * @return the attachments (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + * @see vCal 1.0 p.25 + */ + public List getAttachments() { + return getProperties(Attachment.class); + } + + /** + * Adds an attachment to the to-do task. + * @param attachment the attachment to add + * @see RFC 5545 + * p.80-1 + * @see RFC 2445 + * p.77-8 + * @see vCal 1.0 p.25 + */ + public void addAttachment(Attachment attachment) { + addProperty(attachment); + } + + /** + * Gets the people who are involved in the to-do task. + * @return the attendees (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ + public List getAttendees() { + return getProperties(Attendee.class); + } + + /** + * Adds a person who is involved in the to-do task. + * @param attendee the attendee + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ + public void addAttendee(Attendee attendee) { + addProperty(attendee); + } + + /** + * Adds a person who is involved in the to-do task. + * @param email the attendee's email address + * @return the property that was created + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ + public Attendee addAttendee(String email) { + Attendee prop = new Attendee(null, email, null); + addAttendee(prop); + return prop; + } + + /** + * Gets a list of "tags" or "keywords" that describe the to-do task. + * @return the categories (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public List getCategories() { + return getProperties(Categories.class); + } + + /** + * Adds a list of "tags" or "keywords" that describe the to-do task. Note + * that a single property can hold multiple keywords. + * @param categories the categories to add + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public void addCategories(Categories categories) { + addProperty(categories); + } + + /** + * Adds a list of "tags" or "keywords" that describe the to-do task. + * @param categories the categories to add + * @return the property that was created + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public Categories addCategories(String... categories) { + Categories prop = new Categories(categories); + addCategories(prop); + return prop; + } + + /** + * Adds a list of "tags" or "keywords" that describe the to-do task. + * @param categories the categories to add + * @return the property that was created + * @see RFC 5545 + * p.81-2 + * @see RFC 2445 + * p.78-9 + * @see vCal 1.0 p.28 + */ + public Categories addCategories(List categories) { + Categories prop = new Categories(categories); + addCategories(prop); + return prop; + } + + /** + * Gets the comments attached to the to-do task. + * @return the comments (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public List getComments() { + return getProperties(Comment.class); + } + + /** + * Adds a comment to the to-do task. + * @param comment the comment to add + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public void addComment(Comment comment) { + addProperty(comment); + } + + /** + * Adds a comment to the to-do task. + * @param comment the comment to add + * @return the property that was created + * @see RFC 5545 + * p.83-4 + * @see RFC 2445 + * p.80-1 + */ + public Comment addComment(String comment) { + Comment prop = new Comment(comment); + addComment(prop); + return prop; + } + + /** + * Gets the contacts associated with the to-do task. + * @return the contacts (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public List getContacts() { + return getProperties(Contact.class); + } + + /** + * Adds a contact to the to-do task. + * @param contact the contact + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public void addContact(Contact contact) { + addProperty(contact); + } + + /** + * Adds a contact to the to-do task. + * @param contact the contact (e.g. "ACME Co - (123) 555-1234") + * @return the property that was created + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ + public Contact addContact(String contact) { + Contact prop = new Contact(contact); + addContact(prop); + return prop; + } + + /** + * Gets the list of exceptions to the recurrence rule defined in the to-do + * task (if one is defined). + * @return the list of exceptions (any changes made this list will affect + * the parent component object and vice versa) + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + * @see vCal 1.0 p.31 + */ + public List getExceptionDates() { + return getProperties(ExceptionDates.class); + } + + /** + * Adds a list of exceptions to the recurrence rule defined in the to-do + * task (if one is defined). Note that this property can contain multiple + * dates. + * @param exceptionDates the list of exceptions + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + * @see vCal 1.0 p.31 + */ + public void addExceptionDates(ExceptionDates exceptionDates) { + addProperty(exceptionDates); + } + + /** + * Gets the response to a scheduling request. + * @return the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public RequestStatus getRequestStatus() { + return getProperty(RequestStatus.class); + } + + /** + * Sets the response to a scheduling request. + * @param requestStatus the response + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ + public void setRequestStatus(RequestStatus requestStatus) { + setProperty(RequestStatus.class, requestStatus); + } + + /** + * Gets the components that the to-do task is related to. + * @return the relationships (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ + public List getRelatedTo() { + return getProperties(RelatedTo.class); + } + + /** + * Adds a component that the to-do task is related to. + * @param relatedTo the relationship + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ + public void addRelatedTo(RelatedTo relatedTo) { + //TODO create a method that accepts a component and make the RelatedTo property invisible to the user + //@formatter:off + /* + * addRelation(RelationshipType relType, ICalComponent component) { + * RelatedTo prop = new RelatedTo(component.getUid().getValue()); + * prop.setRelationshipType(relType); + * addProperty(prop); + * } + */ + //@formatter:on + addProperty(relatedTo); + } + + /** + * Adds a component that the to-do task is related to. + * @param uid the UID of the other component + * @return the property that was created + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ + public RelatedTo addRelatedTo(String uid) { + RelatedTo prop = new RelatedTo(uid); + addRelatedTo(prop); + return prop; + } + + /** + * Gets the resources that are needed for the to-do task. + * @return the resources (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public List getResources() { + return getProperties(Resources.class); + } + + /** + * Adds a list of resources that are needed for the to-do task. Note that a + * single property can hold multiple resources. + * @param resources the resources to add + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public void addResources(Resources resources) { + addProperty(resources); + } + + /** + * Adds a list of resources that are needed for the to-do task. + * @param resources the resources to add (e.g. "easel", "projector") + * @return the property that was created + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public Resources addResources(String... resources) { + Resources prop = new Resources(resources); + addResources(prop); + return prop; + } + + /** + * Adds a list of resources that are needed for the to-do task. + * @param resources the resources to add (e.g. "easel", "projector") + * @return the property that was created + * @see RFC 5545 + * p.91 + * @see RFC 2445 + * p.87-8 + * @see vCal 1.0 p.34-5 + */ + public Resources addResources(List resources) { + Resources prop = new Resources(resources); + addResources(prop); + return prop; + } + + /** + * Gets the list of dates/periods that help define the recurrence rule of + * this to-do task (if one is defined). + * @return the recurrence dates (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + * @see vCal 1.0 p.34 + */ + public List getRecurrenceDates() { + return getProperties(RecurrenceDates.class); + } + + /** + * Adds a list of dates/periods that help define the recurrence rule of this + * to-do task (if one is defined). + * @param recurrenceDates the recurrence dates + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + * @see vCal 1.0 p.34 + */ + public void addRecurrenceDates(RecurrenceDates recurrenceDates) { + addProperty(recurrenceDates); + } + + /** + * Gets the alarms that are assigned to this to-do task. + * @return the alarms (any changes made this list will affect the parent + * component object and vice versa) + * @see RFC 5545 + * p.71-6 + * @see RFC 2445 + * p.67-73 + */ + public List getAlarms() { + return getComponents(VAlarm.class); + } + + /** + * Adds an alarm to this to-do task. + * @param alarm the alarm + * @see RFC 5545 + * p.71-6 + * @see RFC 2445 + * p.67-73 + */ + public void addAlarm(VAlarm alarm) { + addComponent(alarm); + } + + /** + *

+ * Gets the exceptions for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @return the exception rules (any changes made this list will affect the + * parent component object and vice versa) + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ + public List getExceptionRules() { + return getProperties(ExceptionRule.class); + } + + /** + *

+ * Adds an exception for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @param recur the exception rule to add + * @return the property that was created + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ + public ExceptionRule addExceptionRule(Recurrence recur) { + ExceptionRule prop = new ExceptionRule(recur); + addExceptionRule(prop); + return prop; + } + + /** + *

+ * Adds an exception for the {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the + * iCal specification. Its use should be avoided. + *

+ * @param exceptionRule the exception rule to add + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ + public void addExceptionRule(ExceptionRule exceptionRule) { + addProperty(exceptionRule); + } + + /** + * Gets the color that clients may use when displaying the to-do task (for + * example, a background color). + * @return the property or null if not set + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color getColor() { + return getProperty(Color.class); + } + + /** + * Sets the color that clients may use when displaying the to-do task (for + * example, a background color). + * @param color the property or null to remove + * @see draft-ietf-calext-extensions-01 + * p.79 + */ + public void setColor(Color color) { + setProperty(Color.class, color); + } + + /** + * Sets the color that clients may use when displaying the to-do task (for + * example, a background color). + * @param color the color name (case insensitive) or null to remove. + * Acceptable values are defined in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + * @return the property object that was created + * @see draft-ietf-calext-extensions-01 + * p.9 + */ + public Color setColor(String color) { + Color prop = (color == null) ? null : new Color(color); + setColor(prop); + return prop; + } + + /** + * Gets the images that are associated with the to-do task. + * @return the images (any changes made this list will affect the parent + * component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public List getImages() { + return getProperties(Image.class); + } + + /** + * Adds an image that is associated with the to-do task. + * @param image the image + * @see draft-ietf-calext-extensions-01 + * p.10 + */ + public void addImage(Image image) { + addProperty(image); + } + + /** + * Gets information related to the to-do task's conference system. + * @return the conferences (any changes made this list will affect the + * parent component object and vice versa) + * @see draft-ietf-calext-extensions-01 + * p.11 + */ + public List getConferences() { + return getProperties(Conference.class); + } + + /** + * Adds information related to the to-do task's conference system. + * @param conference the conference + * @see draft-ietf-calext-extensions-01 + * p.11 + */ + public void addConference(Conference conference) { + addProperty(conference); + } + + /** + *

+ * Creates an iterator that computes the dates defined by the + * {@link RecurrenceRule} and {@link RecurrenceDates} properties (if + * present), and excludes those dates which are defined by the + * {@link ExceptionRule} and {@link ExceptionDates} properties (if present). + *

+ *

+ * In order for {@link RecurrenceRule} and {@link ExceptionRule} properties + * to be included in this iterator, a {@link DateStart} property must be + * defined. + *

+ *

+ * {@link Period} values in {@link RecurrenceDates} properties are not + * supported and are ignored. + *

+ * @param timezone the timezone to iterate in. This is needed in order to + * adjust for when the iterator passes over a daylight savings boundary. + * This parameter is ignored if the start date does not have a time + * component. + * @return the iterator + */ + public DateIterator getDateIterator(TimeZone timezone) { + return Google2445Utils.getDateIterator(this, timezone); + } + + @SuppressWarnings("unchecked") + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (version != ICalVersion.V1_0) { + checkRequiredCardinality(warnings, Uid.class, DateTimeStamp.class); + checkOptionalCardinality(warnings, Classification.class, Completed.class, Created.class, Description.class, DateStart.class, Geo.class, LastModified.class, Location.class, Organizer.class, PercentComplete.class, Priority.class, RecurrenceId.class, Sequence.class, Status.class, Summary.class, Url.class); + } + + checkOptionalCardinality(warnings, Color.class); + + Status[] validStatuses; + switch (version) { + case V1_0: + validStatuses = new Status[] { Status.needsAction(), Status.completed(), Status.accepted(), Status.declined(), Status.delegated(), Status.sent() }; + break; + default: + validStatuses = new Status[] { Status.needsAction(), Status.completed(), Status.inProgress(), Status.cancelled() }; + break; + } + checkStatus(warnings, validStatuses); + + ICalDate dateStart = getValue(getDateStart()); + ICalDate dateDue = getValue(getDateDue()); + if (dateStart != null && dateDue != null) { + //DTSTART must come before DUE + if (dateStart.compareTo(dateDue) > 0) { + warnings.add(new ValidationWarning(22)); + } + + //DTSTART and DUE must have the same data type + if (dateStart.hasTime() != dateDue.hasTime()) { + warnings.add(new ValidationWarning(23)); + } + } + + //DUE and DURATION cannot both exist + DurationProperty duration = getDuration(); + if (dateDue != null && duration != null) { + warnings.add(new ValidationWarning(24)); + } + + //DTSTART is required if DURATION exists + if (dateStart == null && duration != null) { + warnings.add(new ValidationWarning(25)); + } + + //DTSTART and RECURRENCE-ID must have the same data type + ICalDate recurrenceId = getValue(getRecurrenceId()); + if (recurrenceId != null && dateStart != null && dateStart.hasTime() != recurrenceId.hasTime()) { + warnings.add(new ValidationWarning(19)); + } + + //BYHOUR, BYMINUTE, and BYSECOND cannot be specified in RRULE if DTSTART's data type is "date" + //RFC 5545 p. 167 + Recurrence rrule = getValue(getRecurrenceRule()); + if (dateStart != null && rrule != null) { + if (!dateStart.hasTime() && (!rrule.getByHour().isEmpty() || !rrule.getByMinute().isEmpty() || !rrule.getBySecond().isEmpty())) { + warnings.add(new ValidationWarning(5)); + } + } + + //there *should* be only 1 instance of RRULE + //RFC 5545 p. 167 + if (getProperties(RecurrenceRule.class).size() > 1) { + warnings.add(new ValidationWarning(6)); + } + } + + @Override + public VTodo copy() { + return new VTodo(this); + } +} diff --git a/app/src/main/java/biweekly/component/package-info.java b/app/src/main/java/biweekly/component/package-info.java new file mode 100644 index 0000000000..c672418963 --- /dev/null +++ b/app/src/main/java/biweekly/component/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains DTO classes for each component. + */ +package biweekly.component; \ No newline at end of file diff --git a/app/src/main/java/biweekly/google-rfc-2445.license b/app/src/main/java/biweekly/google-rfc-2445.license new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/app/src/main/java/biweekly/google-rfc-2445.license @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/src/main/java/biweekly/io/CannotParseException.java b/app/src/main/java/biweekly/io/CannotParseException.java new file mode 100644 index 0000000000..b3c18ca652 --- /dev/null +++ b/app/src/main/java/biweekly/io/CannotParseException.java @@ -0,0 +1,79 @@ +package biweekly.io; + +import biweekly.Messages; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Thrown during the unmarshalling of an iCalendar property to signal that the + * property's value could not be parsed (for example, being unable to parse a + * date string). + * @author Michael Angstadt + */ +public class CannotParseException extends RuntimeException { + private static final long serialVersionUID = 8299420302297241326L; + private final Integer code; + private final Object args[]; + + /** + * Creates a new "cannot parse" exception. + * @param code the warning message code + * @param args the warning message arguments + */ + public CannotParseException(int code, Object... args) { + this.code = code; + this.args = args; + } + + /** + * Creates a new "cannot parse" exception. + * @param reason the reason why the property value cannot be parsed + */ + public CannotParseException(String reason) { + this(1, reason); + } + + /** + * Gets the warning message code. + * @return the message code + */ + public Integer getCode() { + return code; + } + + /** + * Gets the warning message arguments. + * @return the message arguments + */ + public Object[] getArgs() { + return args; + } + + @Override + public String getMessage() { + return Messages.INSTANCE.getParseMessage(code, args); + } +} diff --git a/app/src/main/java/biweekly/io/DataModelConversionException.java b/app/src/main/java/biweekly/io/DataModelConversionException.java new file mode 100644 index 0000000000..8111fe9be2 --- /dev/null +++ b/app/src/main/java/biweekly/io/DataModelConversionException.java @@ -0,0 +1,81 @@ +package biweekly.io; + +import java.util.ArrayList; +import java.util.List; + +import biweekly.component.ICalComponent; +import biweekly.component.VAlarm; +import biweekly.property.AudioAlarm; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Thrown when a component or property needs to be converted to a different + * component or property when being read or written. For example, converting a + * vCal {@link AudioAlarm} property to a {@link VAlarm} component when parsing a + * vCal file. + * @author Michael Angstadt + */ +public class DataModelConversionException extends RuntimeException { + private static final long serialVersionUID = -4789186852509057375L; + private final ICalProperty originalProperty; + private final List components = new ArrayList(); + private final List properties = new ArrayList(); + + /** + * Creates a conversion exception. + * @param originalProperty the original property object that was parsed or + * null if not applicable + */ + public DataModelConversionException(ICalProperty originalProperty) { + this.originalProperty = originalProperty; + } + + /** + * Gets the original property object that was parsed. + * @return the original property object or null if not applicable + */ + public ICalProperty getOriginalProperty() { + return originalProperty; + } + + /** + * Gets the components that were converted from the original property. + * @return the components + */ + public List getComponents() { + return components; + } + + /** + * Gets the properties that were converted from the original property. + * @return the properties + */ + public List getProperties() { + return properties; + } +} diff --git a/app/src/main/java/biweekly/io/DataModelConverter.java b/app/src/main/java/biweekly/io/DataModelConverter.java new file mode 100644 index 0000000000..317d8282e5 --- /dev/null +++ b/app/src/main/java/biweekly/io/DataModelConverter.java @@ -0,0 +1,241 @@ +package biweekly.io; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TimeZone; + +import biweekly.component.DaylightSavingsTime; +import biweekly.component.Observance; +import biweekly.component.StandardTime; +import biweekly.component.VTimezone; +import biweekly.io.ICalTimeZone.Boundary; +import biweekly.property.Daylight; +import biweekly.property.Timezone; +import biweekly.property.UtcOffsetProperty; +import biweekly.property.ValuedProperty; +import biweekly.util.DateTimeComponents; +import biweekly.util.ICalDate; +import biweekly.util.UtcOffset; +import biweekly.util.com.google.ical.values.DateTimeValue; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Converts various properties/components into other properties/components for + * vCalendar-iCalendar compatibility. + * @author Michael Angstadt + */ +public final class DataModelConverter { + /** + * Converts vCalendar timezone information to an iCalendar {@link VTimezone} + * component. + * @param daylights the DAYLIGHT properties + * @param tz the TZ property + * @return the VTIMEZONE component + */ + public static VTimezone convert(List daylights, Timezone tz) { + UtcOffset tzOffset = ValuedProperty.getValue(tz); + if (daylights.isEmpty() && tzOffset == null) { + return null; + } + + VTimezone timezone = new VTimezone("TZ"); + if (daylights.isEmpty() && tzOffset != null) { + StandardTime st = new StandardTime(); + st.setTimezoneOffsetFrom(tzOffset); + st.setTimezoneOffsetTo(tzOffset); + timezone.addStandardTime(st); + return timezone; + } + + for (Daylight daylight : daylights) { + if (!daylight.isDaylight()) { + continue; + } + + UtcOffset daylightOffset = daylight.getOffset(); + UtcOffset standardOffset = new UtcOffset(daylightOffset.getMillis() - (1000 * 60 * 60)); + + DaylightSavingsTime dst = new DaylightSavingsTime(); + dst.setDateStart(daylight.getStart()); + dst.setTimezoneOffsetFrom(standardOffset); + dst.setTimezoneOffsetTo(daylightOffset); + dst.addTimezoneName(daylight.getDaylightName()); + timezone.addDaylightSavingsTime(dst); + + StandardTime st = new StandardTime(); + st.setDateStart(daylight.getEnd()); + st.setTimezoneOffsetFrom(daylightOffset); + st.setTimezoneOffsetTo(standardOffset); + st.addTimezoneName(daylight.getStandardName()); + timezone.addStandardTime(st); + } + + return timezone.getComponents().isEmpty() ? null : timezone; + } + + /** + * Converts an iCalendar {@link VTimezone} component into the appropriate + * vCalendar properties. + * @param timezone the TIMEZONE component + * @param dates the date values in the vCalendar object that are effected by + * the timezone. + * @return the vCalendar properties + */ + public static VCalTimezoneProperties convert(VTimezone timezone, List dates) { + List daylights = new ArrayList(); + Timezone tz = null; + if (dates.isEmpty()) { + return new VCalTimezoneProperties(daylights, tz); + } + + ICalTimeZone icalTz = new ICalTimeZone(timezone); + Collections.sort(dates); + Set daylightStartDates = new HashSet(); + boolean zeroObservanceUsed = false; + for (Date date : dates) { + Boundary boundary = icalTz.getObservanceBoundary(date); + Observance observance = boundary.getObservanceIn(); + Observance observanceAfter = boundary.getObservanceAfter(); + if (observance == null && observanceAfter == null) { + continue; + } + + if (observance == null) { + //the date comes before the earliest observance + if (observanceAfter instanceof StandardTime && !zeroObservanceUsed) { + UtcOffset offset = getOffset(observanceAfter.getTimezoneOffsetFrom()); + DateTimeValue start = null; + DateTimeValue end = boundary.getObservanceAfterStart(); + String standardName = icalTz.getDisplayName(false, TimeZone.SHORT); + String daylightName = icalTz.getDisplayName(true, TimeZone.SHORT); + + Daylight daylight = new Daylight(true, offset, convert(start), convert(end), standardName, daylightName); + daylights.add(daylight); + zeroObservanceUsed = true; + } + + if (observanceAfter instanceof DaylightSavingsTime) { + UtcOffset offset = getOffset(observanceAfter.getTimezoneOffsetFrom()); + if (offset != null) { + tz = new Timezone(offset); + } + } + + continue; + } + + if (observance instanceof StandardTime) { + UtcOffset offset = getOffset(observance.getTimezoneOffsetTo()); + if (offset != null) { + tz = new Timezone(offset); + } + continue; + } + + if (observance instanceof DaylightSavingsTime && !daylightStartDates.contains(boundary.getObservanceInStart())) { + UtcOffset offset = getOffset(observance.getTimezoneOffsetTo()); + DateTimeValue start = boundary.getObservanceInStart(); + DateTimeValue end = null; + if (observanceAfter != null) { + end = boundary.getObservanceAfterStart(); + } + + String standardName = icalTz.getDisplayName(false, TimeZone.SHORT); + String daylightName = icalTz.getDisplayName(true, TimeZone.SHORT); + + Daylight daylight = new Daylight(true, offset, convert(start), convert(end), standardName, daylightName); + daylights.add(daylight); + daylightStartDates.add(start); + continue; + } + } + + if (tz == null) { + int rawOffset = icalTz.getRawOffset(); + UtcOffset offset = new UtcOffset(rawOffset); + tz = new Timezone(offset); + } + + if (daylights.isEmpty()) { + Daylight daylight = new Daylight(); + daylight.setDaylight(false); + daylights.add(daylight); + } + + return new VCalTimezoneProperties(daylights, tz); + } + + private static UtcOffset getOffset(UtcOffsetProperty property) { + return (property == null) ? null : property.getValue(); + } + + private static ICalDate convert(DateTimeValue value) { + if (value == null) { + return null; + } + + //@formatter:off + DateTimeComponents components = new DateTimeComponents( + value.year(), + value.month(), + value.day(), + value.hour(), + value.minute(), + value.second(), + false + ); + //@formatter:on + + return new ICalDate(components, true); + } + + public static class VCalTimezoneProperties { + private final List daylights; + private final Timezone tz; + + public VCalTimezoneProperties(List daylights, Timezone tz) { + this.daylights = daylights; + this.tz = tz; + } + + public List getDaylights() { + return daylights; + } + + public Timezone getTz() { + return tz; + } + } + + private DataModelConverter() { + //hide + } +} diff --git a/app/src/main/java/biweekly/io/DefaultGlobalTimezoneIdResolver.java b/app/src/main/java/biweekly/io/DefaultGlobalTimezoneIdResolver.java new file mode 100644 index 0000000000..9648fce9c0 --- /dev/null +++ b/app/src/main/java/biweekly/io/DefaultGlobalTimezoneIdResolver.java @@ -0,0 +1,67 @@ +package biweekly.io; + +import biweekly.util.ICalDateFormat; + +import java.util.TimeZone; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Default implementation of {@link GlobalTimezoneIdResolver}. + *

+ *

+ * The following are examples of the kinds of TZID formats this class is able to + * handle. + *

+ *
    + *
  • "TZID=/America/New_York" resolves to + * {@code TimeZone.getTimeZone("America/New_York")}
  • + *
  • "TZID=/mozilla.org/20050126_1/America/New_York" resolves to + * {@code TimeZone.getTimeZone("America/New_York")}
  • + *
+ * @author Michael Angstadt + */ +public class DefaultGlobalTimezoneIdResolver implements GlobalTimezoneIdResolver { + @Override + public TimeZone resolve(String globalId) { + globalId = removeMozillaPrefixIfPresent(globalId); + return ICalDateFormat.parseTimeZoneId(globalId); + } + + /** + * Checks for, and removes, a global ID prefix that Mozilla software adds to + * its iCal files. Googling this prefix returns several search results, + * suggesting it is frequently encountered in the wild. + * @param globalId the global ID (may or may not contain the Mozilla prefix) + * @return the sanitized global ID, or the unchanged ID if it does not + * contain the prefix + */ + private String removeMozillaPrefixIfPresent(String globalId) { + String prefix = "mozilla.org/20050126_1/"; + return globalId.startsWith(prefix) ? globalId.substring(prefix.length()) : globalId; + } +} diff --git a/app/src/main/java/biweekly/io/GlobalTimezoneIdResolver.java b/app/src/main/java/biweekly/io/GlobalTimezoneIdResolver.java new file mode 100644 index 0000000000..337112b25a --- /dev/null +++ b/app/src/main/java/biweekly/io/GlobalTimezoneIdResolver.java @@ -0,0 +1,50 @@ +package biweekly.io; + +import java.util.TimeZone; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Gets Java {@link TimeZone} objects that correspond with TZID parameters that + * contain global timezone IDs (as opposed to IDs that correspond with a + * VTIMEZONE component). + * @author Michael Angstadt + * @see RFC 5545 + * section 3.8.3.1 + * @see RFC 2445 + * section 4.2.19 + */ +public interface GlobalTimezoneIdResolver { + /** + * Returns an appropriate Java {@link TimeZone} object that corresponds to + * the given global ID. + * @param globalId the global ID (the value of the TZID parameter, without + * the forward slash prefix) + * @return the corresponding {@link TimeZone} object or null if the global + * ID is not recognized + */ + TimeZone resolve(String globalId); +} diff --git a/app/src/main/java/biweekly/io/ICalTimeZone.java b/app/src/main/java/biweekly/io/ICalTimeZone.java new file mode 100644 index 0000000000..78e6200869 --- /dev/null +++ b/app/src/main/java/biweekly/io/ICalTimeZone.java @@ -0,0 +1,714 @@ +package biweekly.io; + +import static biweekly.property.ValuedProperty.getValue; +import static biweekly.util.Google2445Utils.convertFromRawComponents; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.TimeZone; + +import biweekly.Messages; +import biweekly.component.DaylightSavingsTime; +import biweekly.component.Observance; +import biweekly.component.StandardTime; +import biweekly.component.VTimezone; +import biweekly.property.ExceptionDates; +import biweekly.property.ExceptionRule; +import biweekly.property.RecurrenceDates; +import biweekly.property.RecurrenceRule; +import biweekly.property.TimezoneName; +import biweekly.util.ICalDate; +import biweekly.util.Recurrence; +import biweekly.util.UtcOffset; +import biweekly.util.com.google.ical.iter.RecurrenceIterator; +import biweekly.util.com.google.ical.iter.RecurrenceIteratorFactory; +import biweekly.util.com.google.ical.util.DTBuilder; +import biweekly.util.com.google.ical.values.DateTimeValue; +import biweekly.util.com.google.ical.values.DateTimeValueImpl; +import biweekly.util.com.google.ical.values.DateValue; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * A timezone that is based on an iCalendar {@link VTimezone} component. This + * class is not thread safe. + * @author Michael Angstadt + */ +@SuppressWarnings("serial") +public class ICalTimeZone extends TimeZone { + private final VTimezone component; + private final Map> observanceDateCache; + final List sortedObservances; + private final int rawOffset; + private final TimeZone utc = TimeZone.getTimeZone("UTC"); + private final Calendar utcCalendar = Calendar.getInstance(utc); + + /** + * Creates a new timezone based on an iCalendar VTIMEZONE component. + * @param component the VTIMEZONE component to wrap + */ + public ICalTimeZone(VTimezone component) { + this.component = component; + + int numObservances = component.getStandardTimes().size() + component.getDaylightSavingsTime().size(); + observanceDateCache = new IdentityHashMap>(numObservances); + + sortedObservances = calculateSortedObservances(); + + rawOffset = calculateRawOffset(); + + String id = getValue(component.getTimezoneId()); + if (id != null) { + setID(id); + } + } + + /** + * Builds a list of all the observances in the VTIMEZONE component, sorted + * by DTSTART. + * @return the sorted observances + */ + private List calculateSortedObservances() { + List daylights = component.getDaylightSavingsTime(); + List standards = component.getStandardTimes(); + + int numObservances = standards.size() + daylights.size(); + List sortedObservances = new ArrayList(numObservances); + + sortedObservances.addAll(standards); + sortedObservances.addAll(daylights); + + Collections.sort(sortedObservances, new Comparator() { + public int compare(Observance left, Observance right) { + ICalDate startLeft = getValue(left.getDateStart()); + ICalDate startRight = getValue(right.getDateStart()); + if (startLeft == null && startRight == null) { + return 0; + } + if (startLeft == null) { + return -1; + } + if (startRight == null) { + return 1; + } + + return startLeft.getRawComponents().compareTo(startRight.getRawComponents()); + } + }); + + return Collections.unmodifiableList(sortedObservances); + } + + @Override + public String getDisplayName(boolean daylight, int style, Locale locale) { + ListIterator it = sortedObservances.listIterator(sortedObservances.size()); + while (it.hasPrevious()) { + Observance observance = it.previous(); + + if (daylight && observance instanceof DaylightSavingsTime) { + List names = observance.getTimezoneNames(); + if (!names.isEmpty()) { + String name = names.get(0).getValue(); + if (name != null) { + return name; + } + } + } + + if (!daylight && observance instanceof StandardTime) { + List names = observance.getTimezoneNames(); + if (!names.isEmpty()) { + String name = names.get(0).getValue(); + if (name != null) { + return name; + } + } + } + } + + return super.getDisplayName(daylight, style, locale); + } + + @Override + public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) { + int hour = millis / 1000 / 60 / 60; + millis -= hour * 1000 * 60 * 60; + int minute = millis / 1000 / 60; + millis -= minute * 1000 * 60; + int second = millis / 1000; + + Observance observance = getObservance(year, month + 1, day, hour, minute, second); + if (observance == null) { + /* + * Find the first observance that has a DTSTART property and a + * TZOFFSETFROM property. + */ + for (Observance obs : sortedObservances) { + ICalDate dateStart = getValue(obs.getDateStart()); + if (dateStart == null) { + continue; + } + + UtcOffset offsetFrom = getValue(obs.getTimezoneOffsetFrom()); + if (offsetFrom == null) { + continue; + } + + return (int) offsetFrom.getMillis(); + } + return 0; + } + + UtcOffset offsetTo = getValue(observance.getTimezoneOffsetTo()); + return (offsetTo == null) ? 0 : (int) offsetTo.getMillis(); + } + + @Override + public int getRawOffset() { + return rawOffset; + } + + private int calculateRawOffset() { + Observance observance = getObservance(new Date()); + if (observance == null) { + //return the offset of the first STANDARD component + for (Observance obs : sortedObservances) { + if (!(obs instanceof StandardTime)) { + continue; + } + + UtcOffset offsetTo = getValue(obs.getTimezoneOffsetTo()); + if (offsetTo == null) { + continue; + } + + return (int) offsetTo.getMillis(); + } + return 0; + } + + UtcOffset offset = getValue((observance instanceof StandardTime) ? observance.getTimezoneOffsetTo() : observance.getTimezoneOffsetFrom()); + return (offset == null) ? 0 : (int) offset.getMillis(); + } + + @Override + public boolean inDaylightTime(Date date) { + if (!useDaylightTime()) { + return false; + } + + Observance observance = getObservance(date); + return (observance == null) ? false : (observance instanceof DaylightSavingsTime); + } + + /** + * This method is not supported by this class. + * @throws UnsupportedOperationException thrown when this method is called + */ + @Override + public void setRawOffset(int offset) { + throw new UnsupportedOperationException(Messages.INSTANCE.getExceptionMessage(12)); + } + + @Override + public boolean useDaylightTime() { + for (Observance observance : sortedObservances) { + if (observance instanceof DaylightSavingsTime) { + return true; + } + } + return false; + } + + /** + * Gets the timezone information of a date. + * @param date the date + * @return the timezone information + */ + public Boundary getObservanceBoundary(Date date) { + utcCalendar.setTime(date); + int year = utcCalendar.get(Calendar.YEAR); + int month = utcCalendar.get(Calendar.MONTH) + 1; + int day = utcCalendar.get(Calendar.DATE); + int hour = utcCalendar.get(Calendar.HOUR); + int minute = utcCalendar.get(Calendar.MINUTE); + int second = utcCalendar.get(Calendar.SECOND); + + return getObservanceBoundary(year, month, day, hour, minute, second); + } + + /** + * Gets the observance that a date is effected by. + * @param date the date + * @return the observance or null if an observance cannot be found + */ + public Observance getObservance(Date date) { + Boundary boundary = getObservanceBoundary(date); + return (boundary == null) ? null : boundary.getObservanceIn(); + } + + /** + *

+ * Gets the VTIMEZONE component that is being wrapped. + *

+ *

+ * Note that the ICalTimeZone class makes heavy use of caching. Any + * modifications made to the VTIMEZONE component that is returned by this + * method may effect the accuracy of this ICalTimeZone instance. + *

+ * @return the VTIMEZONE component + */ + public VTimezone getComponent() { + return component; + } + + /** + * Gets the observance that a date is effected by. + * @param year the year + * @param month the month (1-12) + * @param day the day of the month + * @param hour the hour + * @param minute the minute + * @param second the second + * @return the observance or null if an observance cannot be found + */ + private Observance getObservance(int year, int month, int day, int hour, int minute, int second) { + Boundary boundary = getObservanceBoundary(year, month, day, hour, minute, second); + return (boundary == null) ? null : boundary.getObservanceIn(); + } + + /** + * Gets the observance information of a date. + * @param year the year + * @param month the month (1-12) + * @param day the day of the month + * @param hour the hour + * @param minute the minute + * @param second the second + * @return the observance information or null if none was found + */ + private Boundary getObservanceBoundary(int year, int month, int day, int hour, int minute, int second) { + if (sortedObservances.isEmpty()) { + return null; + } + + DateValue givenTime = new DateTimeValueImpl(year, month, day, hour, minute, second); + int closestIndex = -1; + Observance closest = null; + DateValue closestValue = null; + for (int i = 0; i < sortedObservances.size(); i++) { + Observance observance = sortedObservances.get(i); + + //skip observances that start after the given time + ICalDate dtstart = getValue(observance.getDateStart()); + if (dtstart != null) { + DateValue dtstartValue = convertFromRawComponents(dtstart); + if (dtstartValue.compareTo(givenTime) > 0) { + continue; + } + } + + DateValue dateValue = getObservanceDateClosestToTheGivenDate(observance, givenTime, false); + if (dateValue != null && (closestValue == null || closestValue.compareTo(dateValue) < 0)) { + closestValue = dateValue; + closest = observance; + closestIndex = i; + } + } + + Observance observanceIn = closest; + DateValue observanceInStart = closestValue; + Observance observanceAfter = null; + DateValue observanceAfterStart = null; + if (closestIndex < sortedObservances.size() - 1) { + observanceAfter = sortedObservances.get(closestIndex + 1); + observanceAfterStart = getObservanceDateClosestToTheGivenDate(observanceAfter, givenTime, true); + } + + /* + * If any of the DTSTART properties are missing their time components, + * then observanceInStart/observanceAfterStart could be a DateValue + * object. If so, convert it to a DateTimeValue object (see Issue 77). + */ + if (observanceInStart != null && !(observanceInStart instanceof DateTimeValue)) { + observanceInStart = new DTBuilder(observanceInStart).toDateTime(); + } + if (observanceAfterStart != null && !(observanceAfterStart instanceof DateTimeValue)) { + observanceAfterStart = new DTBuilder(observanceAfterStart).toDateTime(); + } + + return new Boundary((DateTimeValue) observanceInStart, observanceIn, (DateTimeValue) observanceAfterStart, observanceAfter); + } + + /** + * Iterates through each of the timezone boundary dates defined by the given + * observance and finds the date that comes closest to the given date. + * @param observance the observance + * @param givenDate the given date + * @param after true to return the closest date greater than the + * given date, false to return the closest date less than or equal to + * the given date. + * @return the closest date + */ + private DateValue getObservanceDateClosestToTheGivenDate(Observance observance, DateValue givenDate, boolean after) { + List dateCache = observanceDateCache.get(observance); + if (dateCache == null) { + dateCache = new ArrayList(); + observanceDateCache.put(observance, dateCache); + } + + if (dateCache.isEmpty()) { + DateValue prev = null, cur = null; + boolean stopped = false; + RecurrenceIterator it = createIterator(observance); + while (it.hasNext()) { + cur = it.next(); + dateCache.add(cur); + + if (givenDate.compareTo(cur) < 0) { + //stop if we have passed the givenTime + stopped = true; + break; + } + + prev = cur; + } + return after ? (stopped ? cur : null) : prev; + } + + DateValue last = dateCache.get(dateCache.size() - 1); + int comparison = last.compareTo(givenDate); + if ((after && comparison <= 0) || comparison < 0) { + RecurrenceIterator it = createIterator(observance); + + /* + * The "advanceTo()" method skips all dates that are less than the + * given date. I would have thought that we would have to call + * "next()" once because we want it to skip the date that is equal + * to the "last" date. But this causes all the unit tests to fail, + * so I guess not. + */ + it.advanceTo(last); + //it.next(); + + DateValue prev = null, cur = null; + boolean stopped = false; + while (it.hasNext()) { + cur = it.next(); + dateCache.add(cur); + + if (givenDate.compareTo(cur) < 0) { + //stop if we have passed the givenTime + stopped = true; + break; + } + + prev = cur; + } + return after ? (stopped ? cur : null) : prev; + } + + /* + * The date is somewhere in the cached list, so find it. + * + * Note: Read the "binarySearch" method Javadoc carefully for an + * explanation of its return value. + */ + int index = Collections.binarySearch(dateCache, givenDate); + + if (index < 0) { + /* + * The index where the date would be if it was inside the list. + */ + index = (index * -1) - 1; + + if (after) { + /* + * This is where the date would be if it was inside the list, so + * we want to return the date value that's currently at that + * position. + */ + int afterIndex = index; + + return (afterIndex < dateCache.size()) ? dateCache.get(afterIndex) : null; + } + + int beforeIndex = index - 1; + if (beforeIndex < 0) { + return null; + } + if (beforeIndex >= dateCache.size()) { + return dateCache.get(dateCache.size() - 1); + } + return dateCache.get(beforeIndex); + } + + /* + * An exact match was found. + */ + if (after) { + int afterIndex = index + 1; //remember: the date must be > + return (afterIndex < dateCache.size()) ? dateCache.get(afterIndex) : null; + } + return dateCache.get(index); //remember: the date must be <= + } + + /** + * Creates an iterator which iterates over each of the dates in an + * observance. + * @param observance the observance + * @return the iterator + */ + RecurrenceIterator createIterator(Observance observance) { + List inclusions = new ArrayList(); + List exclusions = new ArrayList(); + + ICalDate dtstart = getValue(observance.getDateStart()); + if (dtstart != null) { + DateValue dtstartValue = convertFromRawComponents(dtstart); + + //add DTSTART property + inclusions.add(new DateValueRecurrenceIterator(Collections.singletonList(dtstartValue))); + + //add RRULE properties + for (RecurrenceRule rrule : observance.getProperties(RecurrenceRule.class)) { + Recurrence recur = rrule.getValue(); + if (recur != null) { + inclusions.add(RecurrenceIteratorFactory.createRecurrenceIterator(recur, dtstartValue, utc)); + } + } + + //add EXRULE properties + for (ExceptionRule exrule : observance.getProperties(ExceptionRule.class)) { + Recurrence recur = exrule.getValue(); + if (recur != null) { + exclusions.add(RecurrenceIteratorFactory.createRecurrenceIterator(recur, dtstartValue, utc)); + } + } + } + + //add RDATE properties + List rdates = new ArrayList(); + for (RecurrenceDates rdate : observance.getRecurrenceDates()) { + rdates.addAll(rdate.getDates()); + } + Collections.sort(rdates); + inclusions.add(new DateRecurrenceIterator(rdates)); + + //add EXDATE properties + List exdates = new ArrayList(); + for (ExceptionDates exdate : observance.getProperties(ExceptionDates.class)) { + exdates.addAll(exdate.getValues()); + } + Collections.sort(exdates); + exclusions.add(new DateRecurrenceIterator(exdates)); + + RecurrenceIterator included = join(inclusions); + if (exclusions.isEmpty()) { + return included; + } + + RecurrenceIterator excluded = join(exclusions); + return RecurrenceIteratorFactory.except(included, excluded); + } + + private static RecurrenceIterator join(List iterators) { + if (iterators.isEmpty()) { + return new EmptyRecurrenceIterator(); + } + + RecurrenceIterator first = iterators.get(0); + if (iterators.size() == 1) { + return first; + } + + List theRest = iterators.subList(1, iterators.size()); + return RecurrenceIteratorFactory.join(first, theRest.toArray(new RecurrenceIterator[0])); + } + + /** + * A recurrence iterator that doesn't have any elements. + */ + private static class EmptyRecurrenceIterator implements RecurrenceIterator { + public boolean hasNext() { + return false; + } + + public DateValue next() { + throw new NoSuchElementException(); + } + + public void advanceTo(DateValue newStartUtc) { + //empty + } + + public void remove() { + //RecurrenceIterator does not support this method + throw new UnsupportedOperationException(); + } + } + + /** + * A recurrence iterator that takes a collection of {@link DateValue} + * objects. + */ + private static class DateValueRecurrenceIterator extends IteratorWrapper { + public DateValueRecurrenceIterator(Collection dates) { + super(dates.iterator()); + } + + @Override + protected DateValue toDateValue(DateValue value) { + return value; + } + } + + /** + * A recurrence iterator that takes a collection of {@link ICalDate} + * objects. + */ + private static class DateRecurrenceIterator extends IteratorWrapper { + public DateRecurrenceIterator(Collection dates) { + super(dates.iterator()); + } + + @Override + protected DateValue toDateValue(ICalDate value) { + return convertFromRawComponents(value); + } + } + + /** + * A recurrence iterator that wraps an {@link Iterator}. + */ + private static abstract class IteratorWrapper implements RecurrenceIterator { + protected final Iterator it; + private DateValue next; + + public IteratorWrapper(Iterator it) { + this.it = it; + } + + public DateValue next() { + if (next != null) { + DateValue value = next; + next = null; + return value; + } + return toDateValue(it.next()); + } + + public boolean hasNext() { + return next != null || it.hasNext(); + } + + public void advanceTo(DateValue newStartUtc) { + if (this.next != null && this.next.compareTo(newStartUtc) >= 0) { + return; + } + + while (it.hasNext()) { + DateValue next = toDateValue(it.next()); + if (next.compareTo(newStartUtc) >= 0) { + this.next = next; + break; + } + } + } + + public void remove() { + //RecurrenceIterator does not support this method + throw new UnsupportedOperationException(); + } + + protected abstract DateValue toDateValue(T next); + } + + /** + * Holds the timezone observance information of a particular date. + */ + public static class Boundary { + private final DateTimeValue observanceInStart, observanceAfterStart; + private final Observance observanceIn, observanceAfter; + + public Boundary(DateTimeValue observanceInStart, Observance observanceIn, DateTimeValue observanceAfterStart, Observance observanceAfter) { + this.observanceInStart = observanceInStart; + this.observanceAfterStart = observanceAfterStart; + this.observanceIn = observanceIn; + this.observanceAfter = observanceAfter; + } + + /** + * Gets start time of the observance that the date resides in. + * @return the time + */ + public DateTimeValue getObservanceInStart() { + return observanceInStart; + } + + /** + * Gets the start time the observance that comes after the observance + * that the date resides in. + * @return the time + */ + public DateTimeValue getObservanceAfterStart() { + return observanceAfterStart; + } + + /** + * Gets the observance that the date resides in. + * @return the observance + */ + public Observance getObservanceIn() { + return observanceIn; + } + + /** + * Gets the observance that comes after the observance that the date + * resides in. + * @return the observance + */ + public Observance getObservanceAfter() { + return observanceAfter; + } + + @Override + public String toString() { + return "Boundary [observanceInStart=" + observanceInStart + ", observanceAfterStart=" + observanceAfterStart + ", observanceIn=" + observanceIn + ", observanceAfter=" + observanceAfter + "]"; + } + } +} diff --git a/app/src/main/java/biweekly/io/ParseContext.java b/app/src/main/java/biweekly/io/ParseContext.java new file mode 100644 index 0000000000..8bbf578b3c --- /dev/null +++ b/app/src/main/java/biweekly/io/ParseContext.java @@ -0,0 +1,257 @@ +package biweekly.io; + +import java.util.ArrayList; +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.util.ICalDate; +import biweekly.util.ListMultimap; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Stores information used during the parsing of an iCalendar object. + * @author Michael Angstadt + */ +public class ParseContext { + private ICalVersion version; + private List warnings = new ArrayList(); + private ListMultimap timezonedDates = new ListMultimap(); + private List floatingDates = new ArrayList(); + private Integer lineNumber; + private String propertyName; + + /** + * Gets the version of the iCalendar object being parsed. + * @return the iCalendar version + */ + public ICalVersion getVersion() { + return version; + } + + /** + * Sets the version of the iCalendar object being parsed. + * @param version the iCalendar version + */ + public void setVersion(ICalVersion version) { + this.version = version; + } + + /** + * Gets the line number the parser is currently on. + * @return the line number or null if not applicable + */ + public Integer getLineNumber() { + return lineNumber; + } + + /** + * Sets the line number the parser is currently on. + * @param lineNumber the line number or null if not applicable + */ + public void setLineNumber(Integer lineNumber) { + this.lineNumber = lineNumber; + } + + /** + * Gets the name of the property that the parser is currently parsing. + * @return the property name (e.g. "DTSTART") or null if not applicable + */ + public String getPropertyName() { + return propertyName; + } + + /** + * Sets the name of the property that the parser is currently parsing. + * @param propertyName the property name (e.g. "DTSTART") or null if not + * applicable + */ + public void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + + /** + * Adds a parsed date to this parse context so its timezone can be applied + * to it after the iCalendar object has been parsed (if it has one). + * @param icalDate the parsed date + * @param property the property that the date value belongs to + * @param parameters the property's parameters + */ + public void addDate(ICalDate icalDate, ICalProperty property, ICalParameters parameters) { + if (!icalDate.hasTime()) { + //dates don't have timezones + return; + } + + if (icalDate.getRawComponents().isUtc()) { + //it's a UTC date, so it was already parsed under the correct timezone + return; + } + + //TODO handle UTC offsets within the date strings (not part of iCal standard) + String tzid = parameters.getTimezoneId(); + if (tzid == null) { + addFloatingDate(property, icalDate); + } else { + addTimezonedDate(tzid, property, icalDate); + } + } + + /** + * Keeps track of a date-time property value that uses a timezone so it can + * be parsed later. Timezones cannot be handled until the entire iCalendar + * object has been parsed. + * @param tzid the timezone ID (TZID parameter) + * @param property the property + * @param date the date object that was assigned to the property object + */ + public void addTimezonedDate(String tzid, ICalProperty property, ICalDate date) { + timezonedDates.put(tzid, new TimezonedDate(date, property)); + } + + /** + * Gets the list of date-time property values that use a timezone. + * @return the date-time property values that use a timezone (key = TZID; + * value = the property) + */ + public ListMultimap getTimezonedDates() { + return timezonedDates; + } + + /** + * Keeps track of a date-time property that does not have a timezone + * (floating time), so it can be added to the {@link TimezoneInfo} object + * after the iCalendar object is parsed. + * @param property the property + * @param date the property's date value + */ + public void addFloatingDate(ICalProperty property, ICalDate date) { + floatingDates.add(new TimezonedDate(date, property)); + } + + /** + * Gets the date-time properties that are in floating time (lacking a + * timezone). + * @return the floating date-time properties + */ + public List getFloatingDates() { + return floatingDates; + } + + /** + * Adds a parse warning. + * @param code the warning code + * @param args the warning message arguments + */ + public void addWarning(int code, Object... args) { + //@formatter:off + warnings.add(new ParseWarning.Builder(this) + .message(code, args) + .build()); + //@formatter:on + } + + /** + * Adds a parse warning. + * @param message the warning message + */ + public void addWarning(String message) { + //@formatter:off + warnings.add(new ParseWarning.Builder(this) + .message(message) + .build()); + //@formatter:on + } + + /** + * Gets the parse warnings. + * @return the parse warnings + */ + public List getWarnings() { + return warnings; + } + + /** + * Represents a property whose date-time value has a timezone. + * @author Michael Angstadt + */ + public static class TimezonedDate { + private final ICalDate date; + private final ICalProperty property; + + /** + * @param date the date object that was assigned to the property object + * @param property the property object + */ + public TimezonedDate(ICalDate date, ICalProperty property) { + this.date = date; + this.property = property; + } + + /** + * Gets the date object that was assigned to the property object (should + * be parsed under the JVM's default timezone) + * @return the date object + */ + public ICalDate getDate() { + return date; + } + + /** + * Gets the property object. + * @return the property + */ + public ICalProperty getProperty() { + return property; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((date == null) ? 0 : date.hashCode()); + result = prime * result + ((property == null) ? 0 : property.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + TimezonedDate other = (TimezonedDate) obj; + if (date == null) { + if (other.date != null) return false; + } else if (!date.equals(other.date)) return false; + if (property == null) { + if (other.property != null) return false; + } else if (!property.equals(other.property)) return false; + return true; + } + } +} diff --git a/app/src/main/java/biweekly/io/ParseWarning.java b/app/src/main/java/biweekly/io/ParseWarning.java new file mode 100644 index 0000000000..9d42627180 --- /dev/null +++ b/app/src/main/java/biweekly/io/ParseWarning.java @@ -0,0 +1,186 @@ +package biweekly.io; + +import biweekly.Messages; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a warning that occurred during the parsing of an iCalendar object. + * @author Michael Angstadt + */ +public class ParseWarning { + private final Integer code, lineNumber; + private final String propertyName, message; + + private ParseWarning(Integer lineNumber, String propertyName, Integer code, String message) { + this.lineNumber = lineNumber; + this.propertyName = propertyName; + this.code = code; + this.message = message; + } + + /** + * Gets the warning code. + * @return the warning code or null if no code was specified + */ + public Integer getCode() { + return code; + } + + /** + * Gets the line number the warning occurred on. + * @return the line number or null if not applicable + */ + public Integer getLineNumber() { + return lineNumber; + } + + /** + * Gets the warning message + * @return the warning message + */ + public String getMessage() { + return message; + } + + /** + * Gets the name of the property that the warning occurred on. + * @return the property name (e.g. "DTSTART") or null if not applicable + */ + public String getPropertyName() { + return propertyName; + } + + @Override + public String toString() { + String message = this.message; + if (code != null) { + message = "(" + code + ") " + message; + } + + if (lineNumber == null && propertyName == null) { + return message; + } + + String key = null; + if (lineNumber != null && propertyName == null) { + key = "parse.line"; + } else if (lineNumber == null && propertyName != null) { + key = "parse.prop"; + } else if (lineNumber != null && propertyName != null) { + key = "parse.lineWithProp"; + } + + return Messages.INSTANCE.getMessage(key, lineNumber, propertyName, message); + } + + /** + * Constructs instances of the {@link ParseWarning} class. + * @author Michael Angstadt + */ + public static class Builder { + private Integer lineNumber, code; + private String propertyName, message; + + /** + * Creates an empty builder. + */ + public Builder() { + //empty + } + + /** + * Initializes the builder with data from the parse context. + * @param context the parse context + */ + public Builder(ParseContext context) { + lineNumber(context.getLineNumber()); + propertyName(context.getPropertyName()); + } + + /** + * Sets the name of the property that the warning occurred on. + * @param propertyName the property name (e.g. "DTSTART") or null if not + * applicable + * @return this + */ + public Builder propertyName(String propertyName) { + this.propertyName = propertyName; + return this; + } + + /** + * Sets the line number that the warning occurred on. + * @param lineNumber the line number or null if not applicable + * @return this + */ + public Builder lineNumber(Integer lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + /** + * Sets the warning message. + * @param code the message code + * @param args the message arguments + * @return this + */ + public Builder message(int code, Object... args) { + this.code = code; + message = Messages.INSTANCE.getParseMessage(code, args); + return this; + } + + /** + * Sets the warning message. + * @param message the warning message + * @return this + */ + public Builder message(String message) { + code = null; + this.message = message; + return this; + } + + /** + * Sets the warning message, based on the contents of a + * {@link CannotParseException}. + * @param exception the exception + * @return this + */ + public Builder message(CannotParseException exception) { + return message(exception.getCode(), exception.getArgs()); + } + + /** + * Builds the {@link ParseWarning} object. + * @return the {@link ParseWarning} object + */ + public ParseWarning build() { + return new ParseWarning(lineNumber, propertyName, code, message); + } + } +} diff --git a/app/src/main/java/biweekly/io/SkipMeException.java b/app/src/main/java/biweekly/io/SkipMeException.java new file mode 100644 index 0000000000..622e9a1cec --- /dev/null +++ b/app/src/main/java/biweekly/io/SkipMeException.java @@ -0,0 +1,56 @@ +package biweekly.io; + +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Thrown during the reading or writing of an iCalendar property to show that + * the property should not be written to the iCalendar data stream or not be + * included in the parsed {@link ICalendar} object. + * @author Michael Angstadt + */ +public class SkipMeException extends RuntimeException { + private static final long serialVersionUID = 3384029056232963767L; + private final String reason; + + /** + * Creates a new "skip me" exception. + * @param reason the reason why the property was skipped + */ + public SkipMeException(String reason) { + super(reason); + this.reason = reason; + } + + /** + * Gets the reason why the property was skipped. + * @return the reason + */ + public String getReason() { + return reason; + } +} diff --git a/app/src/main/java/biweekly/io/StreamReader.java b/app/src/main/java/biweekly/io/StreamReader.java new file mode 100644 index 0000000000..505b5ab6b5 --- /dev/null +++ b/app/src/main/java/biweekly/io/StreamReader.java @@ -0,0 +1,398 @@ +package biweekly.io; + +import static biweekly.io.DataModelConverter.convert; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.component.VTimezone; +import biweekly.io.ParseContext.TimezonedDate; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.Daylight; +import biweekly.property.ICalProperty; +import biweekly.property.Timezone; +import biweekly.property.ValuedProperty; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Parses iCalendar objects from a data stream. + * @author Michael Angstadt + */ +public abstract class StreamReader implements Closeable { + protected final List warnings = new ArrayList(); + protected ScribeIndex index = new ScribeIndex(); + protected ParseContext context; + private TimeZone defaultTimezone = TimeZone.getDefault(); + private GlobalTimezoneIdResolver globalTimezoneIdResolver = new DefaultGlobalTimezoneIdResolver(); + + /** + *

+ * Registers an experimental property scribe. Can also be used to override + * the scribe of a standard property (such as DTSTART). Calling this method + * is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)}. + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalPropertyScribe scribe) { + index.register(scribe); + } + + /** + *

+ * Registers an experimental component scribe. Can also be used to override + * the scribe of a standard component (such as VEVENT). Calling this method + * is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)}. + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalComponentScribe scribe) { + index.register(scribe); + } + + /** + * Gets the object that manages the component/property scribes. + * @return the scribe index + */ + public ScribeIndex getScribeIndex() { + return index; + } + + /** + * Sets the object that manages the component/property scribes. + * @param index the scribe index + */ + public void setScribeIndex(ScribeIndex index) { + this.index = index; + } + + /** + * Gets the warnings from the last iCalendar object that was read. + * @return the warnings or empty list if there were no warnings + */ + public List getWarnings() { + return new ArrayList(warnings); + } + + /** + * Gets the timezone that will be used for parsing date property values that + * are floating or that have invalid timezone definitions assigned to them. + * Defaults to {@link TimeZone#getDefault}. + * @return the default timezone + */ + public TimeZone getDefaultTimezone() { + return defaultTimezone; + } + + /** + * Sets the timezone that will be used for parsing date property values that + * are floating or that have invalid timezone definitions assigned to them. + * Defaults to {@link TimeZone#getDefault}. + * @param defaultTimezone the default timezone + */ + public void setDefaultTimezone(TimeZone defaultTimezone) { + this.defaultTimezone = defaultTimezone; + } + + /** + * Gets the resolver that maps global timezone IDs to Java {@link TimeZone} + * objects. Defaults to {@link DefaultGlobalTimezoneIdResolver}. + * @return the resolver + */ + public GlobalTimezoneIdResolver getGlobalTimezoneIdResolver() { + return globalTimezoneIdResolver; + } + + /** + * Sets the resolver that maps global timezone IDs to Java {@link TimeZone} + * objects. Defaults to {@link DefaultGlobalTimezoneIdResolver}. + * @param globalTimezoneIdResolver the resolver + */ + public void setGlobalTimezoneIdResolver(GlobalTimezoneIdResolver globalTimezoneIdResolver) { + this.globalTimezoneIdResolver = globalTimezoneIdResolver; + } + + /** + * Reads all iCalendar objects from the data stream. + * @return the iCalendar objects + * @throws IOException if there's a problem reading from the stream + */ + public List readAll() throws IOException { + List icals = new ArrayList(); + ICalendar ical; + while ((ical = readNext()) != null) { + icals.add(ical); + } + return icals; + } + + /** + * Reads the next iCalendar object from the data stream. + * @return the next iCalendar object or null if there are no more + * @throws IOException if there's a problem reading from the stream + */ + public ICalendar readNext() throws IOException { + warnings.clear(); + context = new ParseContext(); + ICalendar ical = _readNext(); + if (ical == null) { + return null; + } + + ical.setVersion(context.getVersion()); + handleTimezones(ical); + return ical; + } + + /** + * Reads the next iCalendar object from the data stream. + * @return the next iCalendar object or null if there are no more + * @throws IOException if there's a problem reading from the stream + */ + protected abstract ICalendar _readNext() throws IOException; + + private void handleTimezones(ICalendar ical) { + TimezoneInfo tzinfo = ical.getTimezoneInfo(); + + //convert vCalendar DAYLIGHT and TZ properties to a VTIMEZONE component + TimezoneAssignment vcalTimezone = extractVCalTimezone(ical); + + //assign a TimeZone object to each VTIMEZONE component. + Iterator it = ical.getComponents(VTimezone.class).iterator(); + while (it.hasNext()) { + VTimezone component = it.next(); + + //make sure the component has an ID + String id = ValuedProperty.getValue(component.getTimezoneId()); + if (id == null || id.trim().isEmpty()) { + //note: do not remove invalid VTIMEZONE components from the ICalendar object + warnings.add(new ParseWarning.Builder().message(39).build()); + continue; + } + + TimeZone timezone = new ICalTimeZone(component); + tzinfo.getTimezones().add(new TimezoneAssignment(timezone, component)); + + //remove the component from the ICalendar object + it.remove(); + } + + boolean userChangedTheDefaultTimezone = !defaultTimezone.equals(TimeZone.getDefault()); + + if (vcalTimezone != null) { + //vCal: parse floating dates according to the DAYLIGHT and TZ properties (which were converted to a VTIMEZONE component) + Calendar cal = Calendar.getInstance(vcalTimezone.getTimeZone()); + for (TimezonedDate timezonedDate : context.getFloatingDates()) { + reparseDateUnderDifferentTimezone(timezonedDate, cal); + } + } else { + //iCal: treat floating dates as floating dates + for (TimezonedDate timezonedDate : context.getFloatingDates()) { + tzinfo.setFloating(timezonedDate.getProperty(), true); + } + + //convert all floating dates to the default timezone + if (userChangedTheDefaultTimezone) { + Calendar cal = Calendar.getInstance(defaultTimezone); + for (TimezonedDate timezonedDate : context.getFloatingDates()) { + reparseDateUnderDifferentTimezone(timezonedDate, cal); + } + } + } + + //convert all date values to their appropriate timezone + for (Map.Entry> entry : context.getTimezonedDates()) { + String tzid = entry.getKey(); + + //determine which timezone is associated with the given TZID + TimezoneAssignment assignment = determineTimezoneAssignment(tzid, tzinfo); + + /* + * If a timezone assignment could not be found for the given TZID + * and the user did not change the default timezone, then there is + * no need to further process the properties that are assigned to + * this TZID--the date value should remain unchanged (parsed under + * the local machine's default timezone), and its TZID parameter + * should also remain. + */ + if (assignment == null && !userChangedTheDefaultTimezone) { + continue; + } + + //convert each property to the timezone + TimeZone tz = (assignment == null) ? defaultTimezone : assignment.getTimeZone(); + Calendar cal = Calendar.getInstance(tz); + for (TimezonedDate timezonedDate : entry.getValue()) { + ICalProperty property = timezonedDate.getProperty(); + + if (assignment != null) { + tzinfo.setTimezone(property, assignment); + + /* + * Only remove the TZID parameter if the TZID is *valid*. + * Invalid TZID parameters should remain so that user can + * inspect the invalid information. + */ + property.getParameters().setTimezoneId(null); + } + + reparseDateUnderDifferentTimezone(timezonedDate, cal); + } + } + } + + private void reparseDateUnderDifferentTimezone(TimezonedDate timezonedDate, Calendar cal) { + ICalDate date = timezonedDate.getDate(); + + //parse its raw date components under its real timezone + Date realDate = date.getRawComponents().toDate(cal); + + //update the Date object with the new timestamp + date.setTime(realDate.getTime()); + } + + /** + * Determines the timezone definition that is associated with the given ID. + * @param tzid the timezone ID + * @param tzinfo the timezone settings of the iCalendar object + * @return the timezone definition or null to use the default timezone + */ + private TimezoneAssignment determineTimezoneAssignment(String tzid, TimezoneInfo tzinfo) { + boolean isOlsenId = tzid.startsWith("/"); + + //HANDLE OLSEN IDS====================================================== + + if (isOlsenId) { + String globalId = tzid.substring(1); + TimeZone timezone = globalTimezoneIdResolver.resolve(globalId); + if (timezone != null) { + /* + * Olsen ID is valid. Everything is Ok. + */ + TimezoneAssignment assignment = new TimezoneAssignment(timezone, globalId); + tzinfo.getTimezones().add(assignment); + return assignment; + } + + /* + * Even though the TZID is marked as an Olsen ID, and the timezone + * isn't recognized by Java, try looking for a VTIMEZONE component + * that matches it. + * + * This is done as a courtesy and is not required by the specs. + */ + TimezoneAssignment assignment = tzinfo.getTimezoneById(tzid); + int warning; + if (assignment == null) { + /* + * TZID does not match any VTIMEZONE components, use the default + * timezone. + */ + warning = 38; + } else { + warning = 43; + } + + warnings.add(new ParseWarning.Builder().message(warning, tzid).build()); + return assignment; + } + + //HANDLE VTIMEZONE COMPONENT IDS======================================== + + TimezoneAssignment assignment = tzinfo.getTimezoneById(tzid); + if (assignment != null) { + /* + * VTIMEZONE component with the given TZID was found. + * Everything is Ok. + */ + return assignment; + } + + /* + * Try treating the TZID as an Olsen timezone ID, even though it does + * not start with a forward slash. + * + * This is done as a courtesy for users who do not know they must prefix + * Olsen IDs with a forward slash. It is not required by the specs. + */ + String globalId = tzid; + TimeZone timezone = globalTimezoneIdResolver.resolve(globalId); + int warning; + if (timezone == null) { + /* + * TZID is not a valid Olsen ID, use the default timezone. + */ + warning = 38; + assignment = null; + } else { + /* + * TZID was successfully parsed as an Olsen ID. + */ + warning = 37; + assignment = new TimezoneAssignment(timezone, globalId); + tzinfo.getTimezones().add(assignment); + } + + warnings.add(new ParseWarning.Builder().message(warning, globalId).build()); + return assignment; + } + + private TimezoneAssignment extractVCalTimezone(ICalendar ical) { + List daylights = ical.removeProperties(Daylight.class); + List timezones = ical.removeProperties(Timezone.class); + + Timezone timezone = timezones.isEmpty() ? null : timezones.get(0); + VTimezone vcalComponent = convert(daylights, timezone); + if (vcalComponent == null) { + return null; + } + + TimeZone icalTimezone = new ICalTimeZone(vcalComponent); + TimezoneInfo tzinfo = ical.getTimezoneInfo(); + TimezoneAssignment assignment = new TimezoneAssignment(icalTimezone, vcalComponent); + tzinfo.setDefaultTimezone(assignment); + + return assignment; + } +} diff --git a/app/src/main/java/biweekly/io/StreamWriter.java b/app/src/main/java/biweekly/io/StreamWriter.java new file mode 100644 index 0000000000..b8c394f009 --- /dev/null +++ b/app/src/main/java/biweekly/io/StreamWriter.java @@ -0,0 +1,216 @@ +package biweekly.io; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.Messages; +import biweekly.component.ICalComponent; +import biweekly.component.RawComponent; +import biweekly.component.VTimezone; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; +import biweekly.property.RawProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Writes iCalendar objects to a data stream. + * @author Michael Angstadt + */ +public abstract class StreamWriter implements Closeable { + protected ScribeIndex index = new ScribeIndex(); + protected WriteContext context; + protected TimezoneAssignment globalTimezone; + private TimezoneInfo tzinfo; + + /** + * Writes an iCalendar object to the data stream. + * @param ical the iCalendar object to write + * @throws IllegalArgumentException if the scribe class for a component or + * property object cannot be found (only happens when an experimental + * property/component scribe is not registered with the + * {@code registerScribe} method.) + * @throws IOException if there's a problem writing to the data stream + */ + public void write(ICalendar ical) throws IOException { + Collection> unregistered = findScribeless(ical); + if (!unregistered.isEmpty()) { + List classNames = new ArrayList(unregistered.size()); + for (Class clazz : unregistered) { + classNames.add(clazz.getName()); + } + throw Messages.INSTANCE.getIllegalArgumentException(13, classNames); + } + + tzinfo = ical.getTimezoneInfo(); + context = new WriteContext(getTargetVersion(), tzinfo, globalTimezone); + _write(ical); + } + + /** + * Gets the {@link VTimezone} components that need to be written to the + * output stream. + * @return the components + */ + protected Collection getTimezoneComponents() { + if (globalTimezone != null) { + VTimezone component = globalTimezone.getComponent(); + return (component == null) ? Collections. emptyList() : Collections.singletonList(component); + } + + return tzinfo.getComponents(); + } + + /** + * Gets the version that the next iCalendar object will be written as. + * @return the version + */ + protected abstract ICalVersion getTargetVersion(); + + /** + * Writes an iCalendar object to the data stream. + * @param ical the iCalendar object to write + * @throws IOException if there's a problem writing to the data stream + */ + protected abstract void _write(ICalendar ical) throws IOException; + + /** + * Gets the timezone that all date/time property values will be formatted + * in. If set, this setting will override the timezone information + * associated with each {@link ICalendar} object. + * @return the global timezone or null if not set (defaults to null) + */ + public TimezoneAssignment getGlobalTimezone() { + return globalTimezone; + } + + /** + * Sets the timezone that all date/time property values will be formatted + * in. This is a convenience method that overrides the timezone information + * associated with each {@link ICalendar} object that is passed into this + * writer. + * @param globalTimezone the global timezone or null not to set a global + * timezone (defaults to null) + */ + public void setGlobalTimezone(TimezoneAssignment globalTimezone) { + this.globalTimezone = globalTimezone; + } + + /** + *

+ * Registers an experimental property scribe. Can also be used to override + * the scribe of a standard property (such as DTSTART). Calling this method + * is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)}. + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalPropertyScribe scribe) { + index.register(scribe); + } + + /** + *

+ * Registers an experimental component scribe. Can also be used to override + * the scribe of a standard component (such as VEVENT). Calling this method + * is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)}. + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalComponentScribe scribe) { + index.register(scribe); + } + + /** + * Gets the object that manages the component/property scribes. + * @return the scribe index + */ + public ScribeIndex getScribeIndex() { + return index; + } + + /** + * Sets the object that manages the component/property scribes. + * @param scribe the scribe index + */ + public void setScribeIndex(ScribeIndex scribe) { + this.index = scribe; + } + + /** + * Gets the component/property classes that don't have scribes associated + * with them. + * @param ical the iCalendar object + * @return the component/property classes + */ + private Collection> findScribeless(ICalendar ical) { + Set> unregistered = new HashSet>(); + LinkedList components = new LinkedList(); + components.add(ical); + + while (!components.isEmpty()) { + ICalComponent component = components.removeLast(); + + Class componentClass = component.getClass(); + if (componentClass != RawComponent.class && index.getComponentScribe(componentClass) == null) { + unregistered.add(componentClass); + } + + for (Map.Entry, List> entry : component.getProperties()) { + List properties = entry.getValue(); + if (properties.isEmpty()) { + continue; + } + + Class clazz = entry.getKey(); + if (clazz != RawProperty.class && index.getPropertyScribe(clazz) == null) { + unregistered.add(clazz); + } + } + + components.addAll(component.getComponents().values()); + } + + return unregistered; + } +} diff --git a/app/src/main/java/biweekly/io/TimezoneAssignment.java b/app/src/main/java/biweekly/io/TimezoneAssignment.java new file mode 100644 index 0000000000..f5335bc6e3 --- /dev/null +++ b/app/src/main/java/biweekly/io/TimezoneAssignment.java @@ -0,0 +1,127 @@ +package biweekly.io; + +import java.util.TimeZone; + +import biweekly.Messages; +import biweekly.component.VTimezone; +import biweekly.property.TimezoneId; +import biweekly.property.ValuedProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a timezone definition that is used within an iCalendar object. + * @author Michael Angstadt + */ +public class TimezoneAssignment { + private final TimeZone timezone; + private final VTimezone component; + private final String globalId; + + /** + * Creates a timezone that will be inserted into the iCalendar object as a + * VTIMEZONE component. This what most iCalendar files use. + * @param timezone the Java timezone object + * @param component the iCalendar component + * @throws IllegalArgumentException if the given {@link VTimezone} component + * doesn't have a {@link TimezoneId} property + */ + public TimezoneAssignment(TimeZone timezone, VTimezone component) { + String id = ValuedProperty.getValue(component.getTimezoneId()); + if (id == null || id.trim().isEmpty()) { + throw Messages.INSTANCE.getIllegalArgumentException(14); + } + + this.timezone = timezone; + this.component = component; + this.globalId = null; + } + + /** + *

+ * Creates a timezone that will be inserted into the iCalendar object as a + * "global ID". This means that a {@link VTimezone} component containing the + * timezone definition will NOT be inserted into the iCalendar object. + *

+ *

+ * Because the timezone definition is not included inside of the iCalendar + * object, the client consuming the iCalendar object must know how to + * interpret such an ID. The iCalendar specification does not specify a list + * of such IDs, but suggests using the naming convention of an existing + * timezone specification, such as the + * public-domain TZ + * database. + *

+ * @param timezone the Java timezone object + * @param globalId the global ID (e.g. "America/New_York") + */ + public TimezoneAssignment(TimeZone timezone, String globalId) { + this.timezone = timezone; + this.component = null; + this.globalId = globalId; + } + + /** + * Creates a timezone whose VTIMEZONE component is downloaded from + * tzurl.org. + * @param timezone the Java timezone object + * @param outlookCompatible true to download a {@link VTimezone} component + * that is tailored for Microsoft Outlook email clients, false to download a + * standards-based one. + * @return the timezone assignment + * @throws IllegalArgumentException if an appropriate VTIMEZONE component + * cannot be found on the website + */ + public static TimezoneAssignment download(TimeZone timezone, boolean outlookCompatible) { + TzUrlDotOrgGenerator generator = new TzUrlDotOrgGenerator(outlookCompatible); + VTimezone component = generator.generate(timezone); + return new TimezoneAssignment(timezone, component); + } + + /** + * Gets the Java object associated with the timezone. + * @return the Java object + */ + public TimeZone getTimeZone() { + return timezone; + } + + /** + * Gets the iCalendar component associated with the timezone. + * @return the component or null if the timezone uses a global ID + */ + public VTimezone getComponent() { + return component; + } + + /** + * Gets the global ID associated with the timezone. + * @return the global ID or null if the timezone uses an iCalendar component + */ + public String getGlobalId() { + return globalId; + } +} diff --git a/app/src/main/java/biweekly/io/TimezoneInfo.java b/app/src/main/java/biweekly/io/TimezoneInfo.java new file mode 100644 index 0000000000..4ae152648d --- /dev/null +++ b/app/src/main/java/biweekly/io/TimezoneInfo.java @@ -0,0 +1,305 @@ +package biweekly.io; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import biweekly.component.VTimezone; +import biweekly.property.ICalProperty; +import biweekly.property.TimezoneId; +import biweekly.property.ValuedProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Holds the timezone-related settings of an iCalendar object. + * @author Michael Angstadt + */ +public class TimezoneInfo { + @SuppressWarnings("serial") + private final Collection assignments = new HashSet() { + @Override + public boolean remove(Object assignment) { + //remove all property assignments + Collection values = propertyTimezones.values(); + while (values.remove(assignment)) { + //empty + } + + return super.remove(assignment); + } + }; + private final Map propertyTimezones = new IdentityHashMap(); + private final List floatingProperties = new ArrayList(); + + private TimezoneAssignment defaultTimezone; + private boolean globalFloatingTime = false; + + /** + * Gets all the timezones assigned to this object. + * @return the timezones (collection is mutable) + */ + public Collection getTimezones() { + return assignments; + } + + /** + *

+ * Gets the timezone to format all date/time values in (by default, all + * dates are formatted in UTC). + *

+ *

+ * The default timezone is not used for properties that are configured to + * use their own timezone (see {@link #setTimezone}). + *

+ * @return the timezone or null if using UTC + */ + public TimezoneAssignment getDefaultTimezone() { + return defaultTimezone; + } + + /** + *

+ * Sets the timezone to format all date/time values in (by default, all + * dates are formatted in UTC). + *

+ *

+ * The default timezone is not used for properties that are configured to + * use their own timezone (see {@link #setTimezone}). + *

+ * @param timezone the timezone or null to use UTC + */ + public void setDefaultTimezone(TimezoneAssignment timezone) { + if (timezone == null) { + if (defaultTimezone != null && !propertyTimezones.containsValue(defaultTimezone)) { + assignments.remove(defaultTimezone); + } + } else { + assignments.add(timezone); + } + + defaultTimezone = timezone; + } + + /** + * Assigns a timezone to a specific property. + * @param property the property + * @param timezone the timezone or null to format the property according to + * the default timezone (see {@link #setDefaultTimezone}). + */ + public void setTimezone(ICalProperty property, TimezoneAssignment timezone) { + if (timezone == null) { + TimezoneAssignment existing = propertyTimezones.remove(property); + if (existing != null && existing != defaultTimezone && !propertyTimezones.containsValue(existing)) { + assignments.remove(existing); + } + return; + } + + assignments.add(timezone); + propertyTimezones.put(property, timezone); + } + + /** + * Gets the timezone that is assigned to a property. + * @param property the property + * @return the timezone or null if no timezone is assigned to the property + */ + public TimezoneAssignment getTimezone(ICalProperty property) { + return propertyTimezones.get(property); + } + + /** + *

+ * Determines the timezone that a particular property should be formatted in + * when written to an output stream. + *

+ *

+ * Note: You should call {@link #isFloating} first, to determine if the + * property's value is floating (without a timezone). + *

+ * @param property the property + * @return the timezone or null for UTC + */ + public TimezoneAssignment getTimezoneToWriteIn(ICalProperty property) { + TimezoneAssignment assignment = getTimezone(property); + return (assignment == null) ? defaultTimezone : assignment; + } + + /** + * Gets the timezone whose {@link VTimezone} component contains a + * {@link TimezoneId} property with the given value. + * @param tzid the value of the {@link TimezoneId} property + * @return the timezone or null if not found + */ + public TimezoneAssignment getTimezoneById(String tzid) { + for (TimezoneAssignment assignment : assignments) { + VTimezone component = assignment.getComponent(); + if (component == null) { + continue; + } + + String componentId = ValuedProperty.getValue(component.getTimezoneId()); + if (tzid.equals(componentId)) { + return assignment; + } + } + return null; + } + + /** + *

+ * Gets whether to format all date/time values as floating times (defaults + * to false). + *

+ *

+ * This setting does not apply to properties whose floating time settings + * are configured individually (see: {@link #setFloating}) or that are + * configured to use their own timezone (see {@link #setTimezone}). + *

+ *

+ * A floating time value does not have a timezone associated with it, and is + * to be interpreted as being in the local timezone of the computer that is + * consuming the iCalendar object. + *

+ * @return true if enabled, false if disabled + */ + public boolean isGlobalFloatingTime() { + return globalFloatingTime; + } + + /** + *

+ * Sets whether to format all date/time values as floating times (defaults + * to false). + *

+ *

+ * This setting does not apply to properties whose floating time settings + * are configured individually (see: {@link #setFloating}) or that are + * configured to use their own timezone (see {@link #setTimezone}). + *

+ *

+ * A floating time value does not have a timezone associated with it, and is + * to be interpreted as being in the local timezone of the computer that is + * consuming the iCalendar object. + *

+ * @param enable true to enable, false to disable + */ + public void setGlobalFloatingTime(boolean enable) { + globalFloatingTime = enable; + } + + /** + * Determines if a property value should be formatted in floating time when + * written to an output stream. + * @param property the property + * @return true to format in floating time, false not to + */ + public boolean isFloating(ICalProperty property) { + if (containsIdentity(floatingProperties, property)) { + return true; + } + + if (propertyTimezones.containsKey(property)) { + return false; + } + + return globalFloatingTime; + } + + /** + *

+ * Sets whether a property value should be formatted in floating time when + * written to an output stream (by default, floating time is disabled for + * all properties). + *

+ *

+ * A floating time value does not have a timezone associated with it, and is + * to be interpreted as being in the local timezone of the computer that is + * consuming the iCalendar object. + *

+ * @param property the property + * @param enable true to enable floating time for this property, false to + * disable + */ + public void setFloating(ICalProperty property, boolean enable) { + if (enable) { + floatingProperties.add(property); + } else { + removeIdentity(floatingProperties, property); + } + } + + /** + * Gets all of the iCalendar {@link VTimezone} components that have been + * registered with this object. + * @return the components (this collection is immutable) + */ + public Collection getComponents() { + List components = new ArrayList(assignments.size()); + for (TimezoneAssignment assignment : assignments) { + VTimezone component = assignment.getComponent(); + if (component != null) { + components.add(component); + } + } + return Collections.unmodifiableList(components); + } + + /** + * Removes an object from a list using reference equality. + * @param list the list + * @param object the object to remove + */ + private static void removeIdentity(List list, T object) { + Iterator it = list.iterator(); + while (it.hasNext()) { + if (object == it.next()) { + it.remove(); + } + } + } + + /** + * Searches for an item in a list using reference equality. + * @param list the list + * @param object the object to search for + * @return true if the object was found, false if not + */ + private static boolean containsIdentity(List list, T object) { + for (T item : list) { + if (item == object) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/TzUrlDotOrgGenerator.java b/app/src/main/java/biweekly/io/TzUrlDotOrgGenerator.java new file mode 100644 index 0000000000..ab56667ecd --- /dev/null +++ b/app/src/main/java/biweekly/io/TzUrlDotOrgGenerator.java @@ -0,0 +1,158 @@ +package biweekly.io; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import biweekly.ICalendar; +import biweekly.component.VTimezone; +import biweekly.io.text.ICalReader; +import biweekly.property.TimezoneId; +import biweekly.property.ValuedProperty; +import biweekly.util.IOUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Downloads {@link VTimezone} components from tzurl.org. This class is thread-safe. + * @author Michael Angstadt + */ +public class TzUrlDotOrgGenerator { + private static final Map cache = Collections.synchronizedMap(new HashMap()); + private final String baseUrl; + + /** + * Creates a new tzurl.org generator. + * @param outlookCompatible true to download {@link VTimezone} components + * that are tailored for Microsoft Outlook email clients, false to download + * standards-based ones + */ + public TzUrlDotOrgGenerator(boolean outlookCompatible) { + baseUrl = "http://www.tzurl.org/zoneinfo" + (outlookCompatible ? "-outlook" : "") + "/"; + } + + /** + * Generates an iCalendar {@link VTimezone} components from a Java + * {@link TimeZone} object. + * @param timezone the timezone object + * @return the timezone component + * @throws IllegalArgumentException if a timezone definition cannot be found + */ + public VTimezone generate(TimeZone timezone) throws IllegalArgumentException { + URI uri; + try { + uri = new URI(buildUrl(timezone)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + + VTimezone component = cache.get(uri); + if (component != null) { + return component.copy(); + } + + ICalendar ical; + ICalReader reader = null; + try { + reader = new ICalReader(getInputStream(uri)); + ical = reader.readNext(); + } catch (FileNotFoundException e) { + throw notFound(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + IOUtils.closeQuietly(reader); + } + + /* + * There should always be exactly one iCalendar object in the file, but + * check to be sure. + */ + if (ical == null) { + throw notFound(null); + } + + /* + * There should always be exactly one VTIMEZONE component, but check to + * be sure. + */ + TimezoneInfo tzinfo = ical.getTimezoneInfo(); + Collection components = tzinfo.getComponents(); + if (components.isEmpty()) { + components = ical.getComponents(VTimezone.class); //VTIMEZONE components without TZID properties are treated as ordinary components + if (components.isEmpty()) { + throw notFound(null); + } + } + + component = components.iterator().next(); + + /* + * There should always be a TZID property, but just in case there there + * isn't one, create one. + */ + TimezoneId id = component.getTimezoneId(); + if (id == null) { + component.setTimezoneId(timezone.getID()); + } else { + String value = ValuedProperty.getValue(id); + if (value == null || value.trim().isEmpty()) { + id.setValue(timezone.getID()); + } + } + + cache.put(uri, component); + return component.copy(); + } + + private String buildUrl(TimeZone timezone) { + return baseUrl + timezone.getID(); + } + + //for unit testing + InputStream getInputStream(URI uri) throws IOException { + return uri.toURL().openStream(); + } + + /** + * Clears the internal cache of downloaded timezone definitions. + */ + public static void clearCache() { + cache.clear(); + } + + private static IllegalArgumentException notFound(Exception e) { + return new IllegalArgumentException("Timezone ID not recognized.", e); + } +} diff --git a/app/src/main/java/biweekly/io/WriteContext.java b/app/src/main/java/biweekly/io/WriteContext.java new file mode 100644 index 0000000000..a0e9f2bfed --- /dev/null +++ b/app/src/main/java/biweekly/io/WriteContext.java @@ -0,0 +1,117 @@ +package biweekly.io; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Stores information used to write the properties in an iCalendar object. + * @author Michael Angstadt + */ +public class WriteContext { + private final ICalVersion version; + private final TimezoneInfo timezoneOptions; + private final TimezoneAssignment globalTimezone; + private final List dates = new ArrayList(); + private ICalComponent parent; + + public WriteContext(ICalVersion version, TimezoneInfo timezoneOptions, TimezoneAssignment globalTimezone) { + this.version = version; + this.timezoneOptions = timezoneOptions; + this.globalTimezone = globalTimezone; + } + + /** + * Gets the version of the iCalendar object that is being written. + * @return the iCalendar version + */ + public ICalVersion getVersion() { + return version; + } + + /** + * Gets the timezone options for this iCalendar object. + * @return the timezone options + */ + public TimezoneInfo getTimezoneInfo() { + return timezoneOptions; + } + + /** + * Gets the global timezone to format all date/time values in, regardless of + * each individual {@link ICalendar}'s timezone settings. + * @return the global timezone or null if not set + */ + public TimezoneAssignment getGlobalTimezone() { + return globalTimezone; + } + + /** + * Gets the parent component of the property that is being written. + * @return the parent component + */ + public ICalComponent getParent() { + return parent; + } + + /** + * Sets the parent component of the property that is being written. + * @param parent the parent component + */ + public void setParent(ICalComponent parent) { + this.parent = parent; + } + + /** + * Gets the timezoned date-time property values that are in the iCalendar + * object. + * @return the timezoned date-time property values + */ + public List getDates() { + return dates; + } + + /** + * Records the timezoned date-time values that are being written. This is + * used to generate a DAYLIGHT property for vCalendar objects. + * @param floating true if the date is floating, false if not + * @param tz the timezone to format the date in or null for UTC + * @param date the date value + */ + public void addDate(ICalDate date, boolean floating, TimeZone tz) { + if (date != null && date.hasTime() && !floating && tz != null) { + dates.add(date); + } + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingJsonParser.java b/app/src/main/java/biweekly/io/chain/ChainingJsonParser.java new file mode 100644 index 0000000000..8f6ca53648 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingJsonParser.java @@ -0,0 +1,74 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import biweekly.Biweekly; +import biweekly.io.StreamReader; +import biweekly.io.json.JCalReader; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for parsing jCals (JSON-encoded iCalendar objects). + * @see Biweekly#parseJson(InputStream) + * @see Biweekly#parseJson(File) + * @see Biweekly#parseJson(Reader) + * @author Michael Angstadt + */ +public class ChainingJsonParser> extends ChainingParser { + public ChainingJsonParser(String string) { + super(string); + } + + public ChainingJsonParser(InputStream in) { + super(in); + } + + public ChainingJsonParser(Reader reader) { + super(reader); + } + + public ChainingJsonParser(File file) { + super(file); + } + + @Override + StreamReader constructReader() throws IOException { + if (string != null) { + return new JCalReader(string); + } + if (in != null) { + return new JCalReader(in); + } + if (reader != null) { + return new JCalReader(reader); + } + return new JCalReader(file); + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingJsonStringParser.java b/app/src/main/java/biweekly/io/chain/ChainingJsonStringParser.java new file mode 100644 index 0000000000..2354d19f22 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingJsonStringParser.java @@ -0,0 +1,64 @@ +package biweekly.io.chain; + +import java.io.IOException; +import java.util.List; + +import biweekly.Biweekly; +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for parsing jCals (JSON-encoded iCalendar objects) from + * strings. + * @see Biweekly#parseJson(String) + * @author Michael Angstadt + */ +public class ChainingJsonStringParser extends ChainingJsonParser { + public ChainingJsonStringParser(String json) { + super(json); + } + + @Override + public ICalendar first() { + try { + return super.first(); + } catch (IOException e) { + //should never be thrown because we're reading from a string + throw new RuntimeException(e); + } + } + + @Override + public List all() { + try { + return super.all(); + } catch (IOException e) { + //should never be thrown because we're reading from a string + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingJsonWriter.java b/app/src/main/java/biweekly/io/chain/ChainingJsonWriter.java new file mode 100644 index 0000000000..8c4a173f17 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingJsonWriter.java @@ -0,0 +1,154 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.TimeZone; + +import biweekly.Biweekly; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.json.JCalWriter; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for writing jCals (JSON-encoded iCalendar objects). + * @see Biweekly#writeJson(Collection) + * @see Biweekly#writeJson(ICalendar...) + * @author Michael Angstadt + */ +public class ChainingJsonWriter extends ChainingWriter { + private boolean prettyPrint = false; + + /** + * @param icals the iCalendar objects to write + */ + public ChainingJsonWriter(Collection icals) { + super(icals); + } + + /** + * Sets whether or not to pretty-print the JSON. + * @param prettyPrint true to pretty-print it, false not to (defaults to + * false) + * @return this + */ + public ChainingJsonWriter prettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + return this; + } + + @Override + public ChainingJsonWriter tz(TimeZone defaultTimeZone, boolean outlookCompatible) { + return super.tz(defaultTimeZone, outlookCompatible); + } + + @Override + public ChainingJsonWriter register(ICalPropertyScribe scribe) { + return super.register(scribe); + } + + @Override + public ChainingJsonWriter register(ICalComponentScribe scribe) { + return super.register(scribe); + } + + /** + * Writes the iCalendar objects to a string. + * @return the JSON string + */ + public String go() { + StringWriter sw = new StringWriter(); + try { + go(sw); + } catch (IOException e) { + //should never be thrown because we're writing to a string + throw new RuntimeException(e); + } + return sw.toString(); + } + + /** + * Writes the iCalendar objects to an output stream. + * @param out the output stream to write to + * @throws IOException if there's a problem writing to the output stream + */ + public void go(OutputStream out) throws IOException { + go(new JCalWriter(out, wrapInArray())); + } + + /** + * Writes the iCalendar objects to a file. + * @param file the file to write to + * @throws IOException if there's a problem writing to the file + */ + public void go(File file) throws IOException { + JCalWriter writer = new JCalWriter(file, wrapInArray()); + try { + go(writer); + } finally { + writer.close(); + } + } + + /** + * Writes the iCalendar objects to a writer. + * @param writer the writer to write to + * @throws IOException if there's a problem writing to the writer + */ + public void go(Writer writer) throws IOException { + go(new JCalWriter(writer, wrapInArray())); + } + + private void go(JCalWriter writer) throws IOException { + if (defaultTimeZone != null) { + writer.setGlobalTimezone(defaultTimeZone); + } + writer.setPrettyPrint(prettyPrint); + if (index != null) { + writer.setScribeIndex(index); + } + try { + for (ICalendar ical : icals) { + writer.write(ical); + writer.flush(); + } + } finally { + writer.closeJsonStream(); + } + } + + private boolean wrapInArray() { + return icals.size() > 1; + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingParser.java b/app/src/main/java/biweekly/io/chain/ChainingParser.java new file mode 100644 index 0000000000..8ba17653ce --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingParser.java @@ -0,0 +1,206 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; + +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.ParseWarning; +import biweekly.io.StreamReader; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Parent class for all chaining parsers. This class is package-private in order + * to hide it from the generated Javadocs. + * @author Michael Angstadt + * @param the object instance's type (for method chaining) + */ +abstract class ChainingParser> { + final String string; + final InputStream in; + final Reader reader; + final File file; + + ScribeIndex index; + List> warnings; + TimeZone defaultTimezone; + + @SuppressWarnings("unchecked") + final T this_ = (T) this; + + ChainingParser(String string) { + this(string, null, null, null); + } + + ChainingParser(InputStream in) { + this(null, in, null, null); + } + + ChainingParser(Reader reader) { + this(null, null, reader, null); + } + + ChainingParser(File file) { + this(null, null, null, file); + } + + ChainingParser() { + this(null, null, null, null); + } + + private ChainingParser(String string, InputStream in, Reader reader, File file) { + this.string = string; + this.in = in; + this.reader = reader; + this.file = file; + } + + /** + * Registers a property scribe. + * @param scribe the scribe + * @return this + */ + public T register(ICalPropertyScribe scribe) { + if (index == null) { + index = new ScribeIndex(); + } + index.register(scribe); + return this_; + } + + /** + * Registers a component scribe. + * @param scribe the scribe + * @return this + */ + public T register(ICalComponentScribe scribe) { + if (index == null) { + index = new ScribeIndex(); + } + index.register(scribe); + return this_; + } + + /** + * Provides a list object that any parser warnings will be put into. + * @param warnings the list object that will be populated with the warnings + * of each parsed iCalendar object. Each element in the list is a list of + * warnings for one parsed object. Therefore, the size of this list will be + * equal to the number of parsed iCalendar objects. If an iCalendar object + * does not have any warnings, then its warning list will be empty. + * @return this + */ + public T warnings(List> warnings) { + this.warnings = warnings; + return this_; + } + + /** + * Sets the timezone that will be used for parsing date property values that + * are floating or that have invalid timezone definitions assigned to them. + * Defaults to {@link TimeZone#getDefault}. + * @param defaultTimezone the default timezone + * @return this + */ + public T defaultTimezone(TimeZone defaultTimezone) { + this.defaultTimezone = defaultTimezone; + return this_; + } + + /** + * Reads the first iCalendar object from the stream. + * @return the iCalendar object or null if there are none + * @throws IOException if there's an I/O problem + */ + public ICalendar first() throws IOException { + StreamReader reader = constructReader(); + if (index != null) { + reader.setScribeIndex(index); + } + if (defaultTimezone != null) { + reader.setDefaultTimezone(defaultTimezone); + } + + try { + ICalendar ical = reader.readNext(); + if (warnings != null) { + warnings.add(reader.getWarnings()); + } + return ical; + } finally { + if (closeWhenDone()) { + reader.close(); + } + } + } + + /** + * Reads all iCalendar objects from the stream. + * @return the parsed iCalendar objects + * @throws IOException if there's an I/O problem + */ + public List all() throws IOException { + StreamReader reader = constructReader(); + if (index != null) { + reader.setScribeIndex(index); + } + if (defaultTimezone != null) { + reader.setDefaultTimezone(defaultTimezone); + } + + try { + List icals = new ArrayList(); + ICalendar ical; + while ((ical = reader.readNext()) != null) { + if (warnings != null) { + warnings.add(reader.getWarnings()); + } + icals.add(ical); + } + return icals; + } finally { + if (closeWhenDone()) { + reader.close(); + } + } + } + + abstract StreamReader constructReader() throws IOException; + + private boolean closeWhenDone() { + return in == null && reader == null; + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingTextParser.java b/app/src/main/java/biweekly/io/chain/ChainingTextParser.java new file mode 100644 index 0000000000..d0745f5d6e --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingTextParser.java @@ -0,0 +1,97 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import biweekly.Biweekly; +import biweekly.io.StreamReader; +import biweekly.io.text.ICalReader; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for parsing traditional, plain-text iCalendar objects. + * @see Biweekly#parse(InputStream) + * @see Biweekly#parse(File) + * @see Biweekly#parse(Reader) + * @author Michael Angstadt + */ +public class ChainingTextParser> extends ChainingParser { + private boolean caretDecoding = true; + + public ChainingTextParser(String string) { + super(string); + } + + public ChainingTextParser(InputStream in) { + super(in); + } + + public ChainingTextParser(Reader reader) { + super(reader); + } + + public ChainingTextParser(File file) { + super(file); + } + + /** + * Sets whether the reader will decode characters in parameter values that + * use circumflex accent encoding (enabled by default). This only applies to + * version 2.0 iCalendar objects. + * + * @param enable true to use circumflex accent decoding, false not to + * @return this + * @see ICalReader#setCaretDecodingEnabled(boolean) + * @see RFC 6868 + */ + public T caretDecoding(boolean enable) { + caretDecoding = enable; + return this_; + } + + @Override + StreamReader constructReader() throws IOException { + ICalReader reader = newReader(); + reader.setCaretDecodingEnabled(caretDecoding); + return reader; + } + + private ICalReader newReader() throws IOException { + if (string != null) { + return new ICalReader(string); + } + if (in != null) { + return new ICalReader(in); + } + if (reader != null) { + return new ICalReader(reader); + } + return new ICalReader(file); + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingTextStringParser.java b/app/src/main/java/biweekly/io/chain/ChainingTextStringParser.java new file mode 100644 index 0000000000..da8bb368f7 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingTextStringParser.java @@ -0,0 +1,64 @@ +package biweekly.io.chain; + +import java.io.IOException; +import java.util.List; + +import biweekly.Biweekly; +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for parsing traditional, plain-text iCalendar objects from + * strings. + * @see Biweekly#parse(String) + * @author Michael Angstadt + */ +public class ChainingTextStringParser extends ChainingTextParser { + public ChainingTextStringParser(String string) { + super(string); + } + + @Override + public ICalendar first() { + try { + return super.first(); + } catch (IOException e) { + //should never be thrown because we're reading from a string + throw new RuntimeException(e); + } + } + + @Override + public List all() { + try { + return super.all(); + } catch (IOException e) { + //should never be thrown because we're reading from a string + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingTextWriter.java b/app/src/main/java/biweekly/io/chain/ChainingTextWriter.java new file mode 100644 index 0000000000..a3c61ef092 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingTextWriter.java @@ -0,0 +1,233 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.TimeZone; + +import biweekly.Biweekly; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.io.text.ICalWriter; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for writing traditional, plain-text iCalendar objects. + * @see Biweekly#write(Collection) + * @see Biweekly#write(ICalendar...) + * @author Michael Angstadt + */ +public class ChainingTextWriter extends ChainingWriter { + private ICalVersion version; + private boolean caretEncoding = false; + private boolean foldLines = true; + + /** + * @param icals the iCalendar objects to write + */ + public ChainingTextWriter(Collection icals) { + super(icals); + } + + /** + *

+ * Sets the version that all the iCalendar objects will be marshalled to. + * The version that is attached to each individual {@link ICalendar} object + * will be ignored. + *

+ *

+ * If no version is passed into this method, the writer will look at the + * version attached to each individual {@link ICalendar} object and marshal + * it to that version. And if a {@link ICalendar} object has no version + * attached to it, then it will be marshalled to version 2.0. + *

+ * @param version the version to marshal the iCalendar objects to + * @return this + */ + public ChainingTextWriter version(ICalVersion version) { + this.version = version; + return this; + } + + /** + * Sets whether the writer will use circumflex accent encoding for parameter + * values (disabled by default). This only applies to version 2.0 iCalendar + * objects. + * @param enable true to use circumflex accent encoding, false not to + * @return this + * @see ICalWriter#setCaretEncodingEnabled(boolean) + * @see RFC 6868 + */ + public ChainingTextWriter caretEncoding(boolean enable) { + this.caretEncoding = enable; + return this; + } + + /** + *

+ * Sets whether to fold long lines. Line folding is when long lines are + * split up into multiple lines. No data is lost or changed when a line is + * folded. + *

+ *

+ * Line folding is enabled by default. If the iCalendar consumer is not + * parsing your iCalendar objects properly, disabling line folding may help. + *

+ * @param foldLines true to enable line folding, false to disable it + * (defaults to true) + * @return this + */ + public ChainingTextWriter foldLines(boolean foldLines) { + this.foldLines = foldLines; + return this; + } + + @Override + public ChainingTextWriter tz(TimeZone defaultTimeZone, boolean outlookCompatible) { + return super.tz(defaultTimeZone, outlookCompatible); + } + + @Override + public ChainingTextWriter register(ICalPropertyScribe scribe) { + return super.register(scribe); + } + + @Override + public ChainingTextWriter register(ICalComponentScribe scribe) { + return super.register(scribe); + } + + /** + * Writes the iCalendar objects to a string. + * @return the iCalendar string + */ + public String go() { + StringWriter sw = new StringWriter(); + try { + go(sw); + } catch (IOException e) { + //should never be thrown because we're writing to a string + throw new RuntimeException(e); + } + return sw.toString(); + } + + /** + * Writes the iCalendar objects to an output stream. + * @param out the output stream to write to + * @throws IOException if there's a problem writing to the output stream + */ + public void go(OutputStream out) throws IOException { + go(new ICalWriter(out, getICalWriterConstructorVersion())); + } + + /** + * Writes the iCalendar objects to a file. If the file exists, it will be + * overwritten. + * @param file the file to write to + * @throws IOException if there's a problem writing to the file + */ + public void go(File file) throws IOException { + go(file, false); + } + + /** + * Writes the iCalendar objects to a file. + * @param file the file to write to + * @param append true to append onto the end of the file, false to overwrite + * it + * @throws IOException if there's a problem writing to the file + */ + public void go(File file, boolean append) throws IOException { + ICalWriter writer = new ICalWriter(file, append, getICalWriterConstructorVersion()); + try { + go(writer); + } finally { + writer.close(); + } + } + + /** + * Writes the iCalendar objects to a writer. + * @param writer the writer to write to + * @throws IOException if there's a problem writing to the writer + */ + public void go(Writer writer) throws IOException { + go(new ICalWriter(writer, getICalWriterConstructorVersion())); + } + + private void go(ICalWriter writer) throws IOException { + writer.setCaretEncodingEnabled(caretEncoding); + if (!foldLines) { + writer.getVObjectWriter().getFoldedLineWriter().setLineLength(null); + } + if (defaultTimeZone != null) { + writer.setGlobalTimezone(defaultTimeZone); + } + if (index != null) { + writer.setScribeIndex(index); + } + + for (ICalendar ical : icals) { + if (version == null) { + //use the version that's assigned to each individual iCalendar object + ICalVersion icalVersion = ical.getVersion(); + if (icalVersion == null) { + icalVersion = ICalVersion.V2_0; + } + writer.setTargetVersion(icalVersion); + } + writer.write(ical); + writer.flush(); + } + } + + /** + *

+ * Gets the {@link ICalVersion} object to pass into the {@link ICalWriter} + * constructor. The constructor does not allow a null version, so this + * method ensures that a non-null version is passed in. + *

+ *

+ * If the user hasn't chosen a version, the version that is passed into the + * constructor doesn't matter. This is because the writer's target version + * is reset every time an iCalendar object is written (see the + * {@link #go(ICalWriter)} method). + *

+ * @return the version to pass into the constructor + */ + private ICalVersion getICalWriterConstructorVersion() { + return (version == null) ? ICalVersion.V2_0 : version; + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingWriter.java b/app/src/main/java/biweekly/io/chain/ChainingWriter.java new file mode 100644 index 0000000000..2a05b5a14d --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingWriter.java @@ -0,0 +1,108 @@ +package biweekly.io.chain; + +import java.util.Collection; +import java.util.TimeZone; + +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.TimezoneAssignment; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Parent class for all chaining writers. This class is package-private in order + * to hide it from the generated Javadocs. + * @author Michael Angstadt + * @param the object instance's type (for method chaining) + */ +class ChainingWriter> { + final Collection icals; + ScribeIndex index; + // boolean prodId = true; + // boolean versionStrict = true; + TimezoneAssignment defaultTimeZone = null; + + @SuppressWarnings("unchecked") + private final T this_ = (T) this; + + /** + * @param icals the iCalendar objects to write + */ + ChainingWriter(Collection icals) { + this.icals = icals; + } + + /** + *

+ * Sets the timezone to use when outputting date values (defaults to UTC). + *

+ *

+ * This method downloads an appropriate VTIMEZONE component from the tzurl.org website. + *

+ * @param defaultTimeZone the default timezone or null for UTC + * @param outlookCompatible true to download a VTIMEZONE component that is + * tailored for Microsoft Outlook email clients, false to download a + * standards-based one + * @return this + * @throws IllegalArgumentException if an appropriate VTIMEZONE component + * cannot be found on the website + */ + T tz(TimeZone defaultTimeZone, boolean outlookCompatible) { + this.defaultTimeZone = (defaultTimeZone == null) ? null : TimezoneAssignment.download(defaultTimeZone, outlookCompatible); + return this_; + } + + /** + * Registers a property scribe. + * @param scribe the scribe to register + * @return this + */ + T register(ICalPropertyScribe scribe) { + if (index == null) { + index = new ScribeIndex(); + } + index.register(scribe); + return this_; + } + + /** + * Registers a component scribe. + * @param scribe the scribe to register + * @return this + */ + T register(ICalComponentScribe scribe) { + if (index == null) { + index = new ScribeIndex(); + } + index.register(scribe); + return this_; + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingXmlMemoryParser.java b/app/src/main/java/biweekly/io/chain/ChainingXmlMemoryParser.java new file mode 100644 index 0000000000..3f524ab328 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingXmlMemoryParser.java @@ -0,0 +1,71 @@ +package biweekly.io.chain; + +import java.io.IOException; +import java.util.List; + +import org.w3c.dom.Document; + +import biweekly.Biweekly; +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for parsing xCals (XML-encoded iCalendar objects) from strings + * or DOMs. + * @see Biweekly#parseXml(String) + * @see Biweekly#parseXml(Document) + * @author Michael Angstadt + */ +public class ChainingXmlMemoryParser extends ChainingXmlParser { + public ChainingXmlMemoryParser(String xml) { + super(xml); + } + + public ChainingXmlMemoryParser(Document dom) { + super(dom); + } + + @Override + public ICalendar first() { + try { + return super.first(); + } catch (IOException e) { + //should never be thrown because we're reading from a string + throw new RuntimeException(e); + } + } + + @Override + public List all() { + try { + return super.all(); + } catch (IOException e) { + //should never be thrown because we're reading from a string + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingXmlParser.java b/app/src/main/java/biweekly/io/chain/ChainingXmlParser.java new file mode 100644 index 0000000000..96f8451ad5 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingXmlParser.java @@ -0,0 +1,85 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import org.w3c.dom.Document; + +import biweekly.Biweekly; +import biweekly.io.StreamReader; +import biweekly.io.xml.XCalReader; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for parsing xCals (XML-encoded iCalendar objects). + * @see Biweekly#parseXml(InputStream) + * @see Biweekly#parseXml(File) + * @see Biweekly#parseXml(Reader) + * @author Michael Angstadt + */ +public class ChainingXmlParser> extends ChainingParser { + private Document dom; + + public ChainingXmlParser(String string) { + super(string); + } + + public ChainingXmlParser(InputStream in) { + super(in); + } + + public ChainingXmlParser(File file) { + super(file); + } + + public ChainingXmlParser(Reader reader) { + super(reader); + } + + public ChainingXmlParser(Document dom) { + this.dom = dom; + } + + @Override + StreamReader constructReader() throws IOException { + if (string != null) { + return new XCalReader(string); + } + if (in != null) { + return new XCalReader(in); + } + if (reader != null) { + return new XCalReader(reader); + } + if (file != null) { + return new XCalReader(file); + } + return new XCalReader(dom); + } +} diff --git a/app/src/main/java/biweekly/io/chain/ChainingXmlWriter.java b/app/src/main/java/biweekly/io/chain/ChainingXmlWriter.java new file mode 100644 index 0000000000..ede127b9d0 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/ChainingXmlWriter.java @@ -0,0 +1,213 @@ +package biweekly.io.chain; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; + +import org.w3c.dom.Document; + +import biweekly.Biweekly; +import biweekly.ICalDataType; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.io.xml.XCalDocument; +import biweekly.io.xml.XCalDocument.XCalDocumentStreamWriter; +import biweekly.io.xml.XCalOutputProperties; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Chainer class for writing xCal (XML-encoded iCalendar objects). + * @see Biweekly#writeXml(Collection) + * @see Biweekly#writeXml(ICalendar...) + * @author Michael Angstadt + */ +public class ChainingXmlWriter extends ChainingWriter { + private final XCalOutputProperties outputProperties = new XCalOutputProperties(); + private final Map parameterDataTypes = new HashMap(0); + + /** + * @param icals the iCValendar objects to write + */ + public ChainingXmlWriter(Collection icals) { + super(icals); + } + + /** + * Sets the number of indent spaces to use for pretty-printing. If not set, + * then the XML will not be pretty-printed. + * @param indent the number of spaces in the indent string or "null" not to + * pretty-print (disabled by default) + * @return this + */ + public ChainingXmlWriter indent(Integer indent) { + outputProperties.setIndent(indent); + return this; + } + + /** + * Sets the XML version to use. Note that many JDKs only support 1.0 + * natively. For XML 1.1 support, add a JAXP library like xalan to your project. + * @param xmlVersion the XML version (defaults to "1.0") + * @return this + */ + public ChainingXmlWriter xmlVersion(String xmlVersion) { + outputProperties.setXmlVersion(xmlVersion); + return this; + } + + /** + * Assigns an output property to the JAXP transformer (see + * {@link Transformer#setOutputProperty}). + * @param name the property name + * @param value the property value + * @return this + */ + public ChainingXmlWriter outputProperty(String name, String value) { + outputProperties.put(name, value); + return this; + } + + /** + * Assigns all of the given output properties to the JAXP transformer (see + * {@link Transformer#setOutputProperty}). + * @param outputProperties the properties + * @return this + */ + public ChainingXmlWriter outputProperties(Map outputProperties) { + this.outputProperties.putAll(outputProperties); + return this; + } + + @Override + public ChainingXmlWriter tz(TimeZone defaultTimeZone, boolean outlookCompatible) { + return super.tz(defaultTimeZone, outlookCompatible); + } + + @Override + public ChainingXmlWriter register(ICalPropertyScribe scribe) { + return super.register(scribe); + } + + @Override + public ChainingXmlWriter register(ICalComponentScribe scribe) { + return super.register(scribe); + } + + /** + * Registers the data type of a non-standard parameter. Non-standard + * parameters use the "unknown" data type by default. + * @param parameterName the parameter name (e.g. "x-foo") + * @param dataType the data type + * @return this + */ + public ChainingXmlWriter register(String parameterName, ICalDataType dataType) { + parameterDataTypes.put(parameterName, dataType); + return this; + } + + /** + * Writes the iCalendar objects to a string. + * @return the XML document + */ + public String go() { + return createXCalDocument().write(outputProperties); + } + + /** + * Writes the iCalendar objects to an output stream. + * @param out the output stream to write to + * @throws TransformerException if there's a problem writing to the output + * stream + */ + public void go(OutputStream out) throws TransformerException { + createXCalDocument().write(out, outputProperties); + } + + /** + * Writes the iCalendar objects to a file. + * @param file the file to write to + * @throws IOException if the file can't be opened + * @throws TransformerException if there's a problem writing to the file + */ + public void go(File file) throws IOException, TransformerException { + createXCalDocument().write(file, outputProperties); + } + + /** + * Writes the iCalendar objects to a writer. + * @param writer the writer to write to + * @throws TransformerException if there's a problem writing to the writer + */ + public void go(Writer writer) throws TransformerException { + createXCalDocument().write(writer, outputProperties); + } + + /** + * Generates an XML document object model (DOM) containing the iCalendar + * objects. + * @return the DOM + */ + public Document dom() { + return createXCalDocument().getDocument(); + } + + private XCalDocument createXCalDocument() { + XCalDocument document = new XCalDocument(); + + XCalDocumentStreamWriter writer = document.writer(); + if (defaultTimeZone != null) { + writer.setGlobalTimezone(defaultTimeZone); + } + for (Map.Entry entry : parameterDataTypes.entrySet()) { + String parameterName = entry.getKey(); + ICalDataType dataType = entry.getValue(); + writer.registerParameterDataType(parameterName, dataType); + } + if (index != null) { + writer.setScribeIndex(index); + } + + for (ICalendar ical : icals) { + writer.write(ical); + } + + return document; + } +} diff --git a/app/src/main/java/biweekly/io/chain/package-info.java b/app/src/main/java/biweekly/io/chain/package-info.java new file mode 100644 index 0000000000..7e5d5b2784 --- /dev/null +++ b/app/src/main/java/biweekly/io/chain/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains classes used in the chaining API. + * @see biweekly.Biweekly + */ +package biweekly.io.chain; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/json/JCalDeserializer.java b/app/src/main/java/biweekly/io/json/JCalDeserializer.java new file mode 100644 index 0000000000..34d125577b --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalDeserializer.java @@ -0,0 +1,84 @@ +package biweekly.io.json; + +import java.io.IOException; + +import biweekly.ICalendar; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Deserializes jCals within the jackson-databind framework. + * @author Buddy Gorven + * @author Michael Angstadt + */ +public class JCalDeserializer extends JsonDeserializer { + private ScribeIndex index = new ScribeIndex(); + + @Override + public ICalendar deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException { + @SuppressWarnings("resource") + JCalReader reader = new JCalReader(parser); + reader.setScribeIndex(index); + return reader.readNext(); + } + + /** + *

+ * Registers a property scribe. This is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)} + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalPropertyScribe scribe) { + index.register(scribe); + } + + /** + * Gets the scribe index. + * @return the scribe index + */ + public ScribeIndex getScribeIndex() { + return index; + } + + /** + * Sets the scribe index. + * @param index the scribe index + */ + public void setScribeIndex(ScribeIndex index) { + this.index = index; + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalModule.java b/app/src/main/java/biweekly/io/json/JCalModule.java new file mode 100644 index 0000000000..50a8409ca3 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalModule.java @@ -0,0 +1,145 @@ +package biweekly.io.json; + +import biweekly.Biweekly; +import biweekly.ICalendar; +import biweekly.io.TimezoneAssignment; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Module for jackson-databind that serializes and deserializes jCals. + *

+ *

+ * Example: + *

+ * + *
+ * ObjectMapper mapper = new ObjectMapper();
+ * mapper.registerModule(new JCalModule());
+ * ICalendar result = mapper.readValue(..., ICalendar.class);
+ * 
+ * @author Buddy Gorven + * @author Michael Angstadt + */ +public class JCalModule extends SimpleModule { + private static final long serialVersionUID = 8022429868572303471L; + private static final String MODULE_NAME = "biweekly-jcal"; + private static final Version MODULE_VERSION = moduleVersion(); + + private final JCalDeserializer deserializer = new JCalDeserializer(); + private final JCalSerializer serializer = new JCalSerializer(); + + private ScribeIndex index; + + /** + * Creates the module. + */ + public JCalModule() { + super(MODULE_NAME, MODULE_VERSION); + + setScribeIndex(new ScribeIndex()); + addSerializer(serializer); + addDeserializer(ICalendar.class, deserializer); + } + + private static Version moduleVersion() { + String[] split = Biweekly.VERSION.split("[.-]"); + if (split.length < 3) { + /* + * This can happen during development if the "biweekly.properties" + * file has not been filtered by Maven. + */ + return new Version(0, 0, 0, "", Biweekly.GROUP_ID, Biweekly.ARTIFACT_ID); + } + + int major = Integer.parseInt(split[0]); + int minor = Integer.parseInt(split[1]); + int patch = Integer.parseInt(split[2]); + String snapshot = (split.length > 3) ? split[3] : "RELEASE"; + + return new Version(major, minor, patch, snapshot, Biweekly.GROUP_ID, Biweekly.ARTIFACT_ID); + } + + /** + *

+ * Registers a property scribe. This is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)} + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalPropertyScribe scribe) { + index.register(scribe); + } + + /** + * Gets the scribe index used by the serializer and deserializer. + * @return the scribe index + */ + public ScribeIndex getScribeIndex() { + return index; + } + + /** + * Sets the scribe index for the serializer and deserializer to use. + * @param index the scribe index + */ + public void setScribeIndex(ScribeIndex index) { + this.index = index; + serializer.setScribeIndex(index); + deserializer.setScribeIndex(index); + } + + /** + * Gets the timezone that all date/time property values will be formatted + * in. If set, this setting will override the timezone information + * associated with each {@link ICalendar} object. + * @return the global timezone or null if not set (defaults to null) + */ + public TimezoneAssignment getGlobalTimezone() { + return serializer.getGlobalTimezone(); + } + + /** + * Sets the timezone that all date/time property values will be formatted + * in. This is a convenience method that overrides the timezone information + * associated with each {@link ICalendar} object that is passed into this + * writer. + * @param globalTimezone the global timezone or null not to set a global + * timezone (defaults to null) + */ + public void setGlobalTimezone(TimezoneAssignment globalTimezone) { + serializer.setGlobalTimezone(globalTimezone); + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalParseException.java b/app/src/main/java/biweekly/io/json/JCalParseException.java new file mode 100644 index 0000000000..4eacc58079 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalParseException.java @@ -0,0 +1,80 @@ +package biweekly.io.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonToken; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Thrown during the parsing of a JSON-encoded iCalendar object (jCal) when the + * jCal object is not formatted in the correct way (the JSON syntax is valid, + * but it's not in the correct jCal format). + * @author Michael Angstadt + */ +public class JCalParseException extends IOException { + private static final long serialVersionUID = -2447563507966434472L; + private final JsonToken expected, actual; + + /** + * Creates a jCal parse exception. + * @param expected the JSON token that the parser was expecting + * @param actual the actual JSON token + */ + public JCalParseException(JsonToken expected, JsonToken actual) { + super("Expected " + expected + " but was " + actual + "."); + this.expected = expected; + this.actual = actual; + } + + /** + * Creates a jCal parse exception. + * @param message the detail message + * @param expected the JSON token that the parser was expecting + * @param actual the actual JSON token + */ + public JCalParseException(String message, JsonToken expected, JsonToken actual) { + super(message); + this.expected = expected; + this.actual = actual; + } + + /** + * Gets the JSON token that the parser was expected. + * @return the expected token + */ + public JsonToken getExpectedToken() { + return expected; + } + + /** + * Gets the JSON token that was read. + * @return the actual token + */ + public JsonToken getActualToken() { + return actual; + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalPrettyPrinter.java b/app/src/main/java/biweekly/io/json/JCalPrettyPrinter.java new file mode 100644 index 0000000000..17de29b374 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalPrettyPrinter.java @@ -0,0 +1,133 @@ +package biweekly.io.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * A JSON pretty-printer for jCals. + * @author Buddy Gorven + * @author Michael Angstadt + */ +public class JCalPrettyPrinter extends DefaultPrettyPrinter { + private static final long serialVersionUID = 1L; + + /** + * The value that is assigned to {@link JsonGenerator#setCurrentValue} to + * let the pretty-printer know that a iCalendar property is currently being + * written. + */ + public static final Object PROPERTY_VALUE = "ical-property"; + + /** + * Alias for {@link DefaultIndenter#SYSTEM_LINEFEED_INSTANCE} + */ + private static final Indenter NEWLINE_INDENTER = DefaultIndenter.SYSTEM_LINEFEED_INSTANCE; + + /** + * Instance of {@link DefaultPrettyPrinter.FixedSpaceIndenter} + */ + private static final Indenter INLINE_INDENTER = new DefaultPrettyPrinter.FixedSpaceIndenter(); + + private Indenter propertyIndenter, arrayIndenter, objectIndenter; + + public JCalPrettyPrinter() { + propertyIndenter = INLINE_INDENTER; + indentArraysWith(NEWLINE_INDENTER); + indentObjectsWith(NEWLINE_INDENTER); + } + + public JCalPrettyPrinter(JCalPrettyPrinter base) { + super(base); + propertyIndenter = base.propertyIndenter; + indentArraysWith(base.arrayIndenter); + indentObjectsWith(base.objectIndenter); + } + + @Override + public JCalPrettyPrinter createInstance() { + return new JCalPrettyPrinter(this); + } + + @Override + public void indentArraysWith(Indenter indenter) { + arrayIndenter = indenter; + super.indentArraysWith(indenter); + } + + @Override + public void indentObjectsWith(Indenter indenter) { + objectIndenter = indenter; + super.indentObjectsWith(indenter); + } + + public void indentICalPropertiesWith(Indenter indenter) { + propertyIndenter = indenter; + } + + protected static boolean isInICalProperty(JsonStreamContext context) { + if (context == null) { + return false; + } + + Object currentValue = context.getCurrentValue(); + if (currentValue == PROPERTY_VALUE) { + return true; + } + + return isInICalProperty(context.getParent()); + } + + private void updateIndenter(JsonStreamContext context) { + boolean inICalProperty = isInICalProperty(context); + super.indentArraysWith(inICalProperty ? propertyIndenter : arrayIndenter); + super.indentObjectsWith(inICalProperty ? propertyIndenter : objectIndenter); + } + + @Override + public void writeStartArray(JsonGenerator gen) throws IOException, JsonGenerationException { + updateIndenter(gen.getOutputContext().getParent()); + super.writeStartArray(gen); + } + + @Override + public void writeEndArray(JsonGenerator gen, int numValues) throws IOException, JsonGenerationException { + updateIndenter(gen.getOutputContext().getParent()); + super.writeEndArray(gen, numValues); + } + + @Override + public void writeArrayValueSeparator(JsonGenerator gen) throws IOException { + updateIndenter(gen.getOutputContext().getParent()); + super.writeArrayValueSeparator(gen); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/json/JCalRawReader.java b/app/src/main/java/biweekly/io/json/JCalRawReader.java new file mode 100644 index 0000000000..390c5d7529 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalRawReader.java @@ -0,0 +1,335 @@ +package biweekly.io.json; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalDataType; +import biweekly.io.scribe.ScribeIndex; +import biweekly.parameter.ICalParameters; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Parses an iCalendar JSON data stream (jCal). + * @author Michael Angstadt + * @see RFC 7265 + */ +public class JCalRawReader implements Closeable { + private static final String VCALENDAR_COMPONENT_NAME = ScribeIndex.getICalendarScribe().getComponentName().toLowerCase(); //"vcalendar" + + private final Reader reader; + private JsonParser parser; + private boolean eof = false; + private JCalDataStreamListener listener; + private boolean strict = false; + + /** + * @param reader the reader to wrap + */ + public JCalRawReader(Reader reader) { + this.reader = reader; + } + + /** + * @param parser the parser to read from + * @param strict true if the parser's current token is expected to be + * positioned at the start of a jCard, false if not. If this is true, and + * the parser is not positioned at the beginning of a jCard, a + * {@link JCalParseException} will be thrown. If this if false, the parser + * will consume input until it reaches the beginning of a jCard. + */ + public JCalRawReader(JsonParser parser, boolean strict) { + reader = null; + this.parser = parser; + this.strict = strict; + } + + /** + * Gets the current line number. + * @return the line number + */ + public int getLineNum() { + return (parser == null) ? 0 : parser.getCurrentLocation().getLineNr(); + } + + /** + * Reads the next iCalendar object from the jCal data stream. + * @param listener handles the iCalendar data as it is read off the wire + * @throws JCalParseException if the jCal syntax is incorrect (the JSON + * syntax may be valid, but it is not in the correct jCal format). + * @throws JsonParseException if the JSON syntax is incorrect + * @throws IOException if there is a problem reading from the data stream + */ + public void readNext(JCalDataStreamListener listener) throws IOException { + if (parser == null) { + JsonFactory factory = new JsonFactory(); + parser = factory.createParser(reader); + } + + if (parser.isClosed()) { + return; + } + + this.listener = listener; + + //find the next iCalendar object + JsonToken prev = parser.getCurrentToken(); + JsonToken cur; + while ((cur = parser.nextToken()) != null) { + if (prev == JsonToken.START_ARRAY && cur == JsonToken.VALUE_STRING && VCALENDAR_COMPONENT_NAME.equals(parser.getValueAsString())) { + //found + break; + } + + if (strict) { + //the parser was expecting the jCal to be there + if (prev != JsonToken.START_ARRAY) { + throw new JCalParseException(JsonToken.START_ARRAY, prev); + } + + if (cur != JsonToken.VALUE_STRING) { + throw new JCalParseException(JsonToken.VALUE_STRING, cur); + } + + throw new JCalParseException("Invalid value for first token: expected \"vcalendar\" , was \"" + parser.getValueAsString() + "\"", JsonToken.VALUE_STRING, cur); + } + + prev = cur; + } + + if (cur == null) { + //EOF + eof = true; + return; + } + + parseComponent(new ArrayList()); + } + + private void parseComponent(List components) throws IOException { + checkCurrent(JsonToken.VALUE_STRING); + String componentName = parser.getValueAsString(); + listener.readComponent(components, componentName); + components.add(componentName); + + //start properties array + checkNext(JsonToken.START_ARRAY); + + //read properties + while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end properties array + checkCurrent(JsonToken.START_ARRAY); + parser.nextToken(); + parseProperty(components); + } + + //start sub-components array + checkNext(JsonToken.START_ARRAY); + + //read sub-components + while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end sub-components array + checkCurrent(JsonToken.START_ARRAY); + parser.nextToken(); + parseComponent(new ArrayList(components)); + } + + //read the end of the component array (e.g. the last bracket in this example: ["comp", [ /* props */ ], [ /* comps */] ]) + checkNext(JsonToken.END_ARRAY); + } + + private void parseProperty(List components) throws IOException { + //get property name + checkCurrent(JsonToken.VALUE_STRING); + String propertyName = parser.getValueAsString().toLowerCase(); + + ICalParameters parameters = parseParameters(); + + //get data type + checkNext(JsonToken.VALUE_STRING); + String dataTypeStr = parser.getText(); + ICalDataType dataType = "unknown".equals(dataTypeStr) ? null : ICalDataType.get(dataTypeStr); + + //get property value(s) + List values = parseValues(); + + JCalValue value = new JCalValue(values); + listener.readProperty(components, propertyName, parameters, dataType, value); + } + + private ICalParameters parseParameters() throws IOException { + checkNext(JsonToken.START_OBJECT); + + ICalParameters parameters = new ICalParameters(); + while (parser.nextToken() != JsonToken.END_OBJECT) { + String parameterName = parser.getText(); + + if (parser.nextToken() == JsonToken.START_ARRAY) { + //multi-valued parameter + while (parser.nextToken() != JsonToken.END_ARRAY) { + parameters.put(parameterName, parser.getText()); + } + } else { + parameters.put(parameterName, parser.getValueAsString()); + } + } + + return parameters; + } + + private List parseValues() throws IOException { + List values = new ArrayList(); + while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end of the property array + JsonValue value = parseValue(); + values.add(value); + } + return values; + } + + private Object parseValueElement() throws IOException { + switch (parser.getCurrentToken()) { + case VALUE_FALSE: + case VALUE_TRUE: + return parser.getBooleanValue(); + case VALUE_NUMBER_FLOAT: + return parser.getDoubleValue(); + case VALUE_NUMBER_INT: + return parser.getLongValue(); + case VALUE_NULL: + return null; + default: + return parser.getText(); + } + } + + private List parseValueArray() throws IOException { + List array = new ArrayList(); + + while (parser.nextToken() != JsonToken.END_ARRAY) { + JsonValue value = parseValue(); + array.add(value); + } + + return array; + } + + private Map parseValueObject() throws IOException { + Map object = new HashMap(); + + parser.nextToken(); + while (parser.getCurrentToken() != JsonToken.END_OBJECT) { + checkCurrent(JsonToken.FIELD_NAME); + + String key = parser.getText(); + parser.nextToken(); + JsonValue value = parseValue(); + object.put(key, value); + + parser.nextToken(); + } + + return object; + } + + private JsonValue parseValue() throws IOException { + switch (parser.getCurrentToken()) { + case START_ARRAY: + return new JsonValue(parseValueArray()); + case START_OBJECT: + return new JsonValue(parseValueObject()); + default: + return new JsonValue(parseValueElement()); + } + } + + private void checkNext(JsonToken expected) throws IOException { + JsonToken actual = parser.nextToken(); + check(expected, actual); + } + + private void checkCurrent(JsonToken expected) throws JCalParseException { + JsonToken actual = parser.getCurrentToken(); + check(expected, actual); + } + + private void check(JsonToken expected, JsonToken actual) throws JCalParseException { + if (actual != expected) { + throw new JCalParseException(expected, actual); + } + } + + /** + * Determines whether the end of the data stream has been reached. + * @return true if the end has been reached, false if not + */ + public boolean eof() { + return eof; + } + + /** + * Handles the iCalendar data as it is read off the data stream. + * @author Michael Angstadt + */ + public interface JCalDataStreamListener { + /** + * Called when the parser begins to read a component. + * @param parentHierarchy the component's parent components + * @param componentName the component name (e.g. "vevent") + */ + void readComponent(List parentHierarchy, String componentName); + + /** + * Called when a property is read. + * @param componentHierarchy the hierarchy of components that the + * property belongs to + * @param propertyName the property name (e.g. "summary") + * @param parameters the parameters + * @param dataType the data type (e.g. "text") + * @param value the property value + */ + void readProperty(List componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value); + } + + /** + * Closes the underlying {@link Reader} object. + */ + public void close() throws IOException { + if (parser != null) { + parser.close(); + } + if (reader != null) { + reader.close(); + } + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalRawWriter.java b/app/src/main/java/biweekly/io/json/JCalRawWriter.java new file mode 100644 index 0000000000..34c3c2a887 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalRawWriter.java @@ -0,0 +1,364 @@ +package biweekly.io.json; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.Writer; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import biweekly.ICalDataType; +import biweekly.Messages; +import biweekly.parameter.ICalParameters; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonGenerator.Feature; +import com.fasterxml.jackson.core.PrettyPrinter; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Writes data to an iCalendar JSON data stream (jCal). + * @author Michael Angstadt + * @see RFC 7265 + */ +public class JCalRawWriter implements Closeable, Flushable { + private final Writer writer; + private final boolean wrapInArray; + private final LinkedList stack = new LinkedList(); + private JsonGenerator generator; + private boolean prettyPrint = false; + private boolean componentEnded = false; + private boolean closeGenerator = true; + private PrettyPrinter prettyPrinter; + + /** + * @param writer the writer to wrap + * @param wrapInArray true to wrap everything in an array, false not to + * (useful when writing more than one iCalendar object) + */ + public JCalRawWriter(Writer writer, boolean wrapInArray) { + this.writer = writer; + this.wrapInArray = wrapInArray; + } + + /** + * @param generator the generator to write to + */ + public JCalRawWriter(JsonGenerator generator) { + this.writer = null; + this.generator = generator; + this.closeGenerator = false; + this.wrapInArray = false; + } + + /** + * Gets whether or not the JSON will be pretty-printed. + * @return true if it will be pretty-printed, false if not (defaults to + * false) + */ + public boolean isPrettyPrint() { + return prettyPrint; + } + + /** + * Sets whether or not to pretty-print the JSON. + * @param prettyPrint true to pretty-print it, false not to (defaults to + * false) + */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + } + + /** + * Sets the pretty printer to pretty-print the JSON with. Note that this + * method implicitly enables indenting, so {@code setPrettyPrint(true)} does + * not also need to be called. + * @param prettyPrinter the custom pretty printer (defaults to an instance + * of {@link JCalPrettyPrinter}, if {@code setPrettyPrint(true)} has been + * called) + */ + public void setPrettyPrinter(PrettyPrinter prettyPrinter) { + prettyPrint = true; + this.prettyPrinter = prettyPrinter; + } + + /** + * Writes the beginning of a new component array. + * @param componentName the component name (e.g. "vevent") + * @throws IOException if there's an I/O problem + */ + public void writeStartComponent(String componentName) throws IOException { + if (generator == null) { + init(); + } + + componentEnded = false; + + if (!stack.isEmpty()) { + Info parent = stack.getLast(); + if (!parent.wroteEndPropertiesArray) { + generator.writeEndArray(); + parent.wroteEndPropertiesArray = true; + } + if (!parent.wroteStartSubComponentsArray) { + generator.writeStartArray(); + parent.wroteStartSubComponentsArray = true; + } + } + + generator.writeStartArray(); + generator.writeString(componentName); + generator.writeStartArray(); //start properties array + + stack.add(new Info()); + } + + /** + * Closes the current component array. + * @throws IllegalStateException if there are no open components ( + * {@link #writeStartComponent(String)} must be called first) + * @throws IOException if there's an I/O problem + */ + public void writeEndComponent() throws IOException { + if (stack.isEmpty()) { + throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(2)); + } + Info cur = stack.removeLast(); + + if (!cur.wroteEndPropertiesArray) { + generator.writeEndArray(); + } + if (!cur.wroteStartSubComponentsArray) { + generator.writeStartArray(); + } + + generator.writeEndArray(); //end sub-components array + generator.writeEndArray(); //end the array of this component + + componentEnded = true; + } + + /** + * Writes a property to the current component. + * @param propertyName the property name (e.g. "version") + * @param dataType the property's data type (e.g. "text") + * @param value the property value + * @throws IllegalStateException if there are no open components ( + * {@link #writeStartComponent(String)} must be called first) or if the last + * method called was {@link #writeEndComponent()}. + * @throws IOException if there's an I/O problem + */ + public void writeProperty(String propertyName, ICalDataType dataType, JCalValue value) throws IOException { + writeProperty(propertyName, new ICalParameters(), dataType, value); + } + + /** + * Writes a property to the current component. + * @param propertyName the property name (e.g. "version") + * @param parameters the parameters + * @param dataType the property's data type (e.g. "text") + * @param value the property value + * @throws IllegalStateException if there are no open components ( + * {@link #writeStartComponent(String)} must be called first) or if the last + * method called was {@link #writeEndComponent()}. + * @throws IOException if there's an I/O problem + */ + public void writeProperty(String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) throws IOException { + if (stack.isEmpty()) { + throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(2)); + } + if (componentEnded) { + throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(3)); + } + + generator.setCurrentValue(JCalPrettyPrinter.PROPERTY_VALUE); + + generator.writeStartArray(); + + //write the property name + generator.writeString(propertyName); + + //write parameters + generator.writeStartObject(); + for (Map.Entry> entry : parameters) { + String name = entry.getKey().toLowerCase(); + List values = entry.getValue(); + if (values.isEmpty()) { + continue; + } + + if (values.size() == 1) { + generator.writeStringField(name, values.get(0)); + } else { + generator.writeArrayFieldStart(name); + for (String paramValue : values) { + generator.writeString(paramValue); + } + generator.writeEndArray(); + } + } + generator.writeEndObject(); + + //write data type + generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase()); + + //write value + for (JsonValue jsonValue : value.getValues()) { + writeValue(jsonValue); + } + + generator.writeEndArray(); + + generator.setCurrentValue(null); + } + + private void writeValue(JsonValue jsonValue) throws IOException { + if (jsonValue.isNull()) { + generator.writeNull(); + return; + } + + Object val = jsonValue.getValue(); + if (val != null) { + if (val instanceof Byte) { + generator.writeNumber((Byte) val); + } else if (val instanceof Short) { + generator.writeNumber((Short) val); + } else if (val instanceof Integer) { + generator.writeNumber((Integer) val); + } else if (val instanceof Long) { + generator.writeNumber((Long) val); + } else if (val instanceof Float) { + generator.writeNumber((Float) val); + } else if (val instanceof Double) { + generator.writeNumber((Double) val); + } else if (val instanceof Boolean) { + generator.writeBoolean((Boolean) val); + } else { + generator.writeString(val.toString()); + } + return; + } + + List array = jsonValue.getArray(); + if (array != null) { + generator.writeStartArray(); + for (JsonValue element : array) { + writeValue(element); + } + generator.writeEndArray(); + return; + } + + Map object = jsonValue.getObject(); + if (object != null) { + generator.writeStartObject(); + for (Map.Entry entry : object.entrySet()) { + generator.writeFieldName(entry.getKey()); + writeValue(entry.getValue()); + } + generator.writeEndObject(); + return; + } + } + + /** + * Flushes the JSON stream. + * @throws IOException if there's a problem flushing the stream + */ + public void flush() throws IOException { + if (generator == null) { + return; + } + + generator.flush(); + } + + /** + * Finishes writing the JSON document so that it is syntactically correct. + * No more data can be written once this method is called. + * @throws IOException if there's a problem closing the stream + */ + public void closeJsonStream() throws IOException { + if (generator == null) { + return; + } + + while (!stack.isEmpty()) { + writeEndComponent(); + } + + if (wrapInArray) { + generator.writeEndArray(); + } + + if (closeGenerator) { + generator.close(); + } + } + + /** + * Finishes writing the JSON document and closes the underlying + * {@link Writer}. + * @throws IOException if there's a problem closing the stream + */ + public void close() throws IOException { + if (generator == null) { + return; + } + + closeJsonStream(); + + if (writer != null) { + writer.close(); + } + } + + private void init() throws IOException { + JsonFactory factory = new JsonFactory(); + factory.configure(Feature.AUTO_CLOSE_TARGET, false); + generator = factory.createGenerator(writer); + + if (prettyPrint) { + if (prettyPrinter == null) { + prettyPrinter = new JCalPrettyPrinter(); + } + generator.setPrettyPrinter(prettyPrinter); + } + + if (wrapInArray) { + generator.writeStartArray(); + } + } + + private static class Info { + public boolean wroteEndPropertiesArray = false; + public boolean wroteStartSubComponentsArray = false; + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalReader.java b/app/src/main/java/biweekly/io/json/JCalReader.java new file mode 100644 index 0000000000..5371e772a4 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalReader.java @@ -0,0 +1,241 @@ +package biweekly.io.json; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.CannotParseException; +import biweekly.io.ParseWarning; +import biweekly.io.SkipMeException; +import biweekly.io.StreamReader; +import biweekly.io.json.JCalRawReader.JCalDataStreamListener; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.component.ICalendarScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.io.scribe.property.RawPropertyScribe; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.property.RawProperty; +import biweekly.property.Version; +import biweekly.util.Utf8Reader; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Parses {@link ICalendar} objects from a jCal data stream (JSON). + *

+ *

+ * Example: + *

+ * + *
+ * File file = new File("icals.json");
+ * JCalReader reader = null;
+ * try {
+ *   reader = new JCalReader(file);
+ *   ICalendar ical;
+ *   while ((ical = reader.readNext()) != null) {
+ *     //...
+ *   }
+ * } finally {
+ *   if (reader != null) reader.close();
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 7265 + */ +public class JCalReader extends StreamReader { + private static final ICalendarScribe icalScribe = ScribeIndex.getICalendarScribe(); + private final JCalRawReader reader; + + /** + * @param json the JSON string to read from + */ + public JCalReader(String json) { + this(new StringReader(json)); + } + + /** + * @param in the input stream to read from + */ + public JCalReader(InputStream in) { + this(new Utf8Reader(in)); + } + + /** + * @param file the file to read from + * @throws FileNotFoundException if the file doesn't exist + */ + public JCalReader(File file) throws FileNotFoundException { + this(new BufferedReader(new Utf8Reader(file))); + } + + /** + * @param reader the reader to read from + */ + public JCalReader(Reader reader) { + this.reader = new JCalRawReader(reader); + } + + /** + * @param parser the parser to read from + */ + public JCalReader(JsonParser parser) { + this.reader = new JCalRawReader(parser, true); + } + + /** + * Reads the next iCalendar object from the JSON data stream. + * @return the iCalendar object or null if there are no more + * @throws JCalParseException if the jCal syntax is incorrect (the JSON + * syntax may be valid, but it is not in the correct jCal format). + * @throws JsonParseException if the JSON syntax is incorrect + * @throws IOException if there is a problem reading from the data stream + */ + @Override + public ICalendar _readNext() throws IOException { + if (reader.eof()) { + return null; + } + + context.setVersion(ICalVersion.V2_0); + + JCalDataStreamListenerImpl listener = new JCalDataStreamListenerImpl(); + reader.readNext(listener); + + return listener.getICalendar(); + } + + //@Override + public void close() throws IOException { + reader.close(); + } + + private class JCalDataStreamListenerImpl implements JCalDataStreamListener { + private final Map, ICalComponent> components = new HashMap, ICalComponent>(); + + public void readProperty(List componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) { + context.getWarnings().clear(); + context.setLineNumber(reader.getLineNum()); + context.setPropertyName(propertyName); + + //get the component that the property belongs to + ICalComponent parent = components.get(componentHierarchy); + + //unmarshal the property + ICalPropertyScribe scribe = index.getPropertyScribe(propertyName, ICalVersion.V2_0); + try { + ICalProperty property = scribe.parseJson(value, dataType, parameters, context); + warnings.addAll(context.getWarnings()); + + //set "ICalendar.version" if the value of the VERSION property is recognized + //otherwise, unmarshal VERSION like a normal property + if (parent instanceof ICalendar && property instanceof Version) { + Version version = (Version) property; + ICalVersion icalVersion = version.toICalVersion(); + if (icalVersion != null) { + context.setVersion(icalVersion); + return; + } + } + + parent.addProperty(property); + } catch (SkipMeException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(0, e.getMessage()) + .build() + ); + //@formatter:on + } catch (CannotParseException e) { + RawProperty property = new RawPropertyScribe(propertyName).parseJson(value, dataType, parameters, context); + parent.addProperty(property); + + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(e) + .build() + ); + //@formatter:on + } + } + + public void readComponent(List parentHierarchy, String componentName) { + ICalComponentScribe scribe = index.getComponentScribe(componentName, ICalVersion.V2_0); + ICalComponent component = scribe.emptyInstance(); + + ICalComponent parent = components.get(parentHierarchy); + if (parent != null) { + parent.addComponent(component); + } + + List hierarchy = new ArrayList(parentHierarchy); + hierarchy.add(componentName); + components.put(hierarchy, component); + } + + public ICalendar getICalendar() { + if (components.isEmpty()) { + //EOF + return null; + } + + ICalComponent component = components.get(Collections.singletonList(icalScribe.getComponentName().toLowerCase())); + if (component == null) { + //should never happen because the parser always looks for a "vcalendar" component + return null; + } + + if (component instanceof ICalendar) { + //should happen every time + return (ICalendar) component; + } + + //this will only happen if the user decides to override the ICalendarScribe for some reason + ICalendar ical = icalScribe.emptyInstance(); + ical.addComponent(component); + return ical; + } + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalSerializer.java b/app/src/main/java/biweekly/io/json/JCalSerializer.java new file mode 100644 index 0000000000..f006b9d12c --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalSerializer.java @@ -0,0 +1,116 @@ +package biweekly.io.json; + +import java.io.IOException; + +import biweekly.ICalendar; +import biweekly.io.TimezoneAssignment; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.property.ICalProperty; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Serializes jCals within the jackson-databind framework. + * @author Buddy Gorven + * @author Michael Angstadt + */ +@JsonFormat +public class JCalSerializer extends StdSerializer { + private static final long serialVersionUID = 8964681078186049817L; + private ScribeIndex index = new ScribeIndex(); + private TimezoneAssignment globalTimezone; + + public JCalSerializer() { + super(ICalendar.class); + } + + @Override + public void serialize(ICalendar value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { + @SuppressWarnings("resource") + JCalWriter writer = new JCalWriter(gen); + writer.setScribeIndex(getScribeIndex()); + writer.setGlobalTimezone(globalTimezone); + writer.write(value); + } + + /** + *

+ * Registers a property scribe. This is the same as calling: + *

+ *

+ * {@code getScribeIndex().register(scribe)} + *

+ * @param scribe the scribe to register + */ + public void registerScribe(ICalPropertyScribe scribe) { + index.register(scribe); + } + + /** + * Gets the scribe index. + * @return the scribe index + */ + public ScribeIndex getScribeIndex() { + return index; + } + + /** + * Sets the scribe index. + * @param index the scribe index + */ + public void setScribeIndex(ScribeIndex index) { + this.index = index; + } + + /** + * Gets the timezone that all date/time property values will be formatted + * in. If set, this setting will override the timezone information + * associated with each {@link ICalendar} object. + * @return the global timezone or null if not set (defaults to null) + */ + public TimezoneAssignment getGlobalTimezone() { + return globalTimezone; + } + + /** + * Sets the timezone that all date/time property values will be formatted + * in. This is a convenience method that overrides the timezone information + * associated with each {@link ICalendar} object that is passed into this + * writer. + * @param globalTimezone the global timezone or null not to set a global + * timezone (defaults to null) + */ + public void setGlobalTimezone(TimezoneAssignment globalTimezone) { + this.globalTimezone = globalTimezone; + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalValue.java b/app/src/main/java/biweekly/io/json/JCalValue.java new file mode 100644 index 0000000000..087912a3f4 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalValue.java @@ -0,0 +1,360 @@ +package biweekly.io.json; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.util.ListMultimap; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Holds the value of a jCal property. + * @author Michael Angstadt + */ +public class JCalValue { + private final List values; + + /** + * Creates a new jCal value. + * @param values the values + */ + public JCalValue(List values) { + this.values = Collections.unmodifiableList(values); + } + + /** + * Creates a new jCal value. + * @param values the values + */ + public JCalValue(JsonValue... values) { + this.values = Arrays.asList(values); //unmodifiable + } + + /** + * Creates a single-valued value. + * @param value the value + * @return the jCal value + */ + public static JCalValue single(Object value) { + return new JCalValue(new JsonValue(value)); + } + + /** + * Creates a multi-valued value. + * @param values the values + * @return the jCal value + */ + public static JCalValue multi(Object... values) { + return multi(Arrays.asList(values)); + } + + /** + * Creates a multi-valued value. + * @param values the values + * @return the jCal value + */ + public static JCalValue multi(List values) { + List multiValues = new ArrayList(values.size()); + for (Object value : values) { + multiValues.add(new JsonValue(value)); + } + return new JCalValue(multiValues); + } + + /** + *

+ * Creates a structured value. + *

+ *

+ * This method accepts a vararg of {@link Object} instances. {@link List} + * objects will be treated as multi-valued components. All other objects. + * Null values will be treated as empty components. + *

+ * @param values the values + * @return the jCal value + */ + public static JCalValue structured(Object... values) { + List> valuesList = new ArrayList>(values.length); + for (Object value : values) { + List list = (value instanceof List) ? (List) value : Collections.singletonList(value); + valuesList.add(list); + } + return structured(valuesList); + } + + /** + * Creates a structured value. + * @param values the values + * @return the jCal value + */ + public static JCalValue structured(List> values) { + List array = new ArrayList(values.size()); + + for (List list : values) { + if (list.isEmpty()) { + array.add(new JsonValue("")); + continue; + } + + if (list.size() == 1) { + Object value = list.get(0); + if (value == null) { + value = ""; + } + array.add(new JsonValue(value)); + continue; + } + + List subArray = new ArrayList(list.size()); + for (Object value : list) { + if (value == null) { + value = ""; + } + subArray.add(new JsonValue(value)); + } + array.add(new JsonValue(subArray)); + } + + return new JCalValue(new JsonValue(array)); + } + + /** + * Creates an object value. + * @param value the object + * @return the jCal value + */ + public static JCalValue object(ListMultimap value) { + Map object = new LinkedHashMap(); + for (Map.Entry> entry : value) { + String key = entry.getKey(); + List list = entry.getValue(); + + JsonValue v; + if (list.size() == 1) { + v = new JsonValue(list.get(0)); + } else { + List array = new ArrayList(list.size()); + for (Object element : list) { + array.add(new JsonValue(element)); + } + v = new JsonValue(array); + } + object.put(key, v); + } + return new JCalValue(new JsonValue(object)); + } + + /** + * Gets the raw JSON values. Use one of the "{@code as*}" methods to parse + * the values as one of the standard jCal values. + * @return the JSON values + */ + public List getValues() { + return values; + } + + /** + * Parses this jCal value as a single-valued property value. + * @return the value or empty string if not found + */ + public String asSingle() { + if (values.isEmpty()) { + return ""; + } + + JsonValue first = values.get(0); + if (first.isNull()) { + return ""; + } + + Object obj = first.getValue(); + if (obj != null) { + return obj.toString(); + } + + //get the first element of the array + List array = first.getArray(); + if (array != null && !array.isEmpty()) { + obj = array.get(0).getValue(); + if (obj != null) { + return obj.toString(); + } + } + + return ""; + } + + /** + * Parses this jCal value as a structured property value. + * @return the structured values or empty list if not found + */ + public List> asStructured() { + if (values.isEmpty()) { + return Collections.emptyList(); + } + + JsonValue first = values.get(0); + + //["request-status", {}, "text", ["2.0", "Success"] ] + List array = first.getArray(); + if (array != null) { + List> components = new ArrayList>(array.size()); + for (JsonValue value : array) { + if (value.isNull()) { + components.add(Collections.emptyList()); + continue; + } + + Object obj = value.getValue(); + if (obj != null) { + String s = obj.toString(); + List component = s.isEmpty() ? Collections.emptyList() : Collections.singletonList(s); + components.add(component); + continue; + } + + List subArray = value.getArray(); + if (subArray != null) { + List component = new ArrayList(subArray.size()); + for (JsonValue subArrayValue : subArray) { + if (subArrayValue.isNull()) { + component.add(""); + continue; + } + + obj = subArrayValue.getValue(); + if (obj != null) { + component.add(obj.toString()); + continue; + } + } + if (component.size() == 1 && component.get(0).isEmpty()) { + component.clear(); + } + components.add(component); + } + } + return components; + } + + //get the first value if it's not enclosed in an array + //["request-status", {}, "text", "2.0"] + Object obj = first.getValue(); + if (obj != null) { + List> components = new ArrayList>(1); + String s = obj.toString(); + List component = s.isEmpty() ? Collections.emptyList() : Collections.singletonList(s); + components.add(component); + return components; + } + + //["request-status", {}, "text", null] + if (first.isNull()) { + List> components = new ArrayList>(1); + components.add(Collections.emptyList()); + return components; + } + + return Collections.emptyList(); + } + + /** + * Parses this jCal value as a multi-valued property value. + * @return the values or empty list if not found + */ + public List asMulti() { + if (values.isEmpty()) { + return Collections.emptyList(); + } + + List multi = new ArrayList(values.size()); + for (JsonValue value : values) { + if (value.isNull()) { + multi.add(""); + continue; + } + + Object obj = value.getValue(); + if (obj != null) { + multi.add(obj.toString()); + continue; + } + } + return multi; + } + + /** + * Parses this jCal value as an object property value. + * @return the object or an empty map if not found + */ + public ListMultimap asObject() { + if (values.isEmpty()) { + return new ListMultimap(0); + } + + Map map = values.get(0).getObject(); + if (map == null) { + return new ListMultimap(0); + } + + ListMultimap values = new ListMultimap(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + JsonValue value = entry.getValue(); + + if (value.isNull()) { + values.put(key, ""); + continue; + } + + Object obj = value.getValue(); + if (obj != null) { + values.put(key, obj.toString()); + continue; + } + + List array = value.getArray(); + if (array != null) { + for (JsonValue element : array) { + if (element.isNull()) { + values.put(key, ""); + continue; + } + + obj = element.getValue(); + if (obj != null) { + values.put(key, obj.toString()); + } + } + } + } + return values; + } +} diff --git a/app/src/main/java/biweekly/io/json/JCalWriter.java b/app/src/main/java/biweekly/io/json/JCalWriter.java new file mode 100644 index 0000000000..4c52be4e1c --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JCalWriter.java @@ -0,0 +1,250 @@ +package biweekly.io.json; + +import java.io.File; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Collection; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.component.VTimezone; +import biweekly.io.SkipMeException; +import biweekly.io.StreamWriter; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.property.Version; +import biweekly.util.Utf8Writer; + +import com.fasterxml.jackson.core.JsonGenerator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Writes {@link ICalendar} objects to a JSON data stream (jCal). + *

+ *

+ * Example: + *

+ * + *
+ * ICalendar ical1 = ...
+ * ICalendar ical2 = ...
+ * File file = new File("icals.json");
+ * JCalWriter writer = null;
+ * try {
+ *   writer = new JCalWriter(file);
+ *   writer.write(ical1);
+ *   writer.write(ical2);
+ * } finally {
+ *   if (writer != null) writer.close();
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 7265 + */ +public class JCalWriter extends StreamWriter implements Flushable { + private final JCalRawWriter writer; + private final ICalVersion targetVersion = ICalVersion.V2_0; + + /** + * @param out the output stream to write to (UTF-8 encoding will be used) + */ + public JCalWriter(OutputStream out) { + this(new Utf8Writer(out)); + } + + /** + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param wrapInArray true to wrap all iCalendar objects in a parent array, + * false not to (useful when writing more than one iCalendar object) + */ + public JCalWriter(OutputStream out, boolean wrapInArray) { + this(new Utf8Writer(out), wrapInArray); + } + + /** + * @param file the file to write to (UTF-8 encoding will be used) + * @throws IOException if the file cannot be written to + */ + public JCalWriter(File file) throws IOException { + this(new Utf8Writer(file)); + } + + /** + * @param file the file to write to (UTF-8 encoding will be used) + * @param wrapInArray true to wrap all iCalendar objects in a parent array, + * false not to (useful when writing more than one iCalendar object) + * @throws IOException if the file cannot be written to + */ + public JCalWriter(File file, boolean wrapInArray) throws IOException { + this(new Utf8Writer(file), wrapInArray); + } + + /** + * @param writer the writer to write to + */ + public JCalWriter(Writer writer) { + this(writer, false); + } + + /** + * @param writer the writer to write to + * @param wrapInArray true to wrap all iCalendar objects in a parent array, + * false not to (useful when writing more than one iCalendar object) + */ + public JCalWriter(Writer writer, boolean wrapInArray) { + this.writer = new JCalRawWriter(writer, wrapInArray); + } + + /** + * @param generator the generator to write to + */ + public JCalWriter(JsonGenerator generator) { + this.writer = new JCalRawWriter(generator); + } + + /** + * Gets whether or not the JSON will be pretty-printed. + * @return true if it will be pretty-printed, false if not (defaults to + * false) + */ + public boolean isPrettyPrint() { + return writer.isPrettyPrint(); + } + + /** + * Sets whether or not to pretty-print the JSON. + * @param prettyPrint true to pretty-print it, false not to (defaults to + * false) + */ + public void setPrettyPrint(boolean prettyPrint) { + writer.setPrettyPrint(prettyPrint); + } + + @Override + protected void _write(ICalendar ical) throws IOException { + writeComponent(ical); + } + + @Override + protected ICalVersion getTargetVersion() { + return targetVersion; + } + + /** + * Writes a component to the data stream. + * @param component the component to write + * @throws IllegalArgumentException if the scribe class for a component or + * property object cannot be found (only happens when an experimental + * property/component scribe is not registered with the + * {@code registerScribe} method.) + * @throws IOException if there's a problem writing to the data stream + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void writeComponent(ICalComponent component) throws IOException { + ICalComponentScribe componentScribe = index.getComponentScribe(component); + writer.writeStartComponent(componentScribe.getComponentName().toLowerCase()); + + List propertyObjs = componentScribe.getProperties(component); + if (component instanceof ICalendar && component.getProperty(Version.class) == null) { + propertyObjs.add(0, new Version(targetVersion)); + } + + //write properties + for (Object propertyObj : propertyObjs) { + context.setParent(component); //set parent here incase a scribe resets the parent + ICalProperty property = (ICalProperty) propertyObj; + ICalPropertyScribe propertyScribe = index.getPropertyScribe(property); + + //marshal property + ICalParameters parameters; + JCalValue value; + try { + parameters = propertyScribe.prepareParameters(property, context); + value = propertyScribe.writeJson(property, context); + } catch (SkipMeException e) { + continue; + } + + //write property + String propertyName = propertyScribe.getPropertyName(targetVersion).toLowerCase(); + ICalDataType dataType = propertyScribe.dataType(property, targetVersion); + writer.writeProperty(propertyName, parameters, dataType, value); + } + + //write sub-components + List subComponents = componentScribe.getComponents(component); + if (component instanceof ICalendar) { + //add the VTIMEZONE components that were auto-generated by TimezoneOptions + Collection tzs = getTimezoneComponents(); + for (VTimezone tz : tzs) { + if (!subComponents.contains(tz)) { + subComponents.add(0, tz); + } + } + } + for (Object subComponentObj : subComponents) { + ICalComponent subComponent = (ICalComponent) subComponentObj; + writeComponent(subComponent); + } + + writer.writeEndComponent(); + } + + /** + * Flushes the stream. + * @throws IOException if there's a problem flushing the stream + */ + public void flush() throws IOException { + writer.flush(); + } + + /** + * Finishes writing the JSON document and closes the underlying + * {@link Writer} object. + * @throws IOException if there's a problem closing the stream + */ + public void close() throws IOException { + writer.close(); + } + + /** + * Finishes writing the JSON document so that it is syntactically correct. + * No more iCalendar objects can be written once this method is called. + * @throws IOException if there's a problem writing to the data stream + */ + public void closeJsonStream() throws IOException { + writer.closeJsonStream(); + } +} diff --git a/app/src/main/java/biweekly/io/json/JsonValue.java b/app/src/main/java/biweekly/io/json/JsonValue.java new file mode 100644 index 0000000000..729f51793d --- /dev/null +++ b/app/src/main/java/biweekly/io/json/JsonValue.java @@ -0,0 +1,166 @@ +package biweekly.io.json; + +import java.util.List; +import java.util.Map; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a JSON value, array, or object. + * @author Michael Angstadt + */ +public class JsonValue { + private final boolean isNull; + private final Object value; + private final List array; + private final Map object; + + /** + * Creates a JSON value (such as a string or integer). + * @param value the value + */ + public JsonValue(Object value) { + this.value = value; + array = null; + object = null; + isNull = (value == null); + } + + /** + * Creates a JSON array. + * @param array the array elements + */ + public JsonValue(List array) { + this.array = array; + value = null; + object = null; + isNull = (array == null); + } + + /** + * Creates a JSON object. + * @param object the object fields + */ + public JsonValue(Map object) { + this.object = object; + value = null; + array = null; + isNull = (object == null); + } + + /** + * Gets the JSON value. + * @return the value or null if it's not a JSON value + */ + public Object getValue() { + return value; + } + + /** + * Gets the JSON array elements. + * @return the array elements or null if it's not a JSON array + */ + public List getArray() { + return array; + } + + /** + * Gets the JSON object. + * @return the object or null if it's not a JSON object + */ + public Map getObject() { + return object; + } + + /** + * Determines if the value is "null" or not. + * @return true if the value is "null", false if not + */ + public boolean isNull() { + return isNull; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((array == null) ? 0 : array.hashCode()); + result = prime * result + (isNull ? 1231 : 1237); + result = prime * result + ((object == null) ? 0 : object.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonValue other = (JsonValue) obj; + if (array == null) { + if (other.array != null) + return false; + } else if (!array.equals(other.array)) + return false; + if (isNull != other.isNull) + return false; + if (object == null) { + if (other.object != null) + return false; + } else if (!object.equals(other.object)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + if (isNull) { + return "NULL"; + } + + if (value != null) { + return "VALUE = " + value; + } + + if (array != null) { + return "ARRAY = " + array; + } + + if (object != null) { + return "OBJECT = " + object; + } + + return ""; + } +} diff --git a/app/src/main/java/biweekly/io/json/package-info.java b/app/src/main/java/biweekly/io/json/package-info.java new file mode 100644 index 0000000000..ce1a29df11 --- /dev/null +++ b/app/src/main/java/biweekly/io/json/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for reading and writing jCals (JSON-encoded iCalendar objects). + */ +package biweekly.io.json; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/package-info.java b/app/src/main/java/biweekly/io/package-info.java new file mode 100644 index 0000000000..4b115d5669 --- /dev/null +++ b/app/src/main/java/biweekly/io/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains I/O related classes. + */ +package biweekly.io; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/ScribeIndex.java b/app/src/main/java/biweekly/io/scribe/ScribeIndex.java new file mode 100644 index 0000000000..417a7b66b9 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/ScribeIndex.java @@ -0,0 +1,451 @@ +package biweekly.io.scribe; + +import java.util.HashMap; +import java.util.Map; + +import javax.xml.namespace.QName; + +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.component.RawComponent; +import biweekly.io.scribe.component.DaylightSavingsTimeScribe; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.component.ICalendarScribe; +import biweekly.io.scribe.component.RawComponentScribe; +import biweekly.io.scribe.component.StandardTimeScribe; +import biweekly.io.scribe.component.VAlarmScribe; +import biweekly.io.scribe.component.VEventScribe; +import biweekly.io.scribe.component.VFreeBusyScribe; +import biweekly.io.scribe.component.VJournalScribe; +import biweekly.io.scribe.component.VTimezoneScribe; +import biweekly.io.scribe.component.VTodoScribe; +import biweekly.io.scribe.property.ActionScribe; +import biweekly.io.scribe.property.AttachmentScribe; +import biweekly.io.scribe.property.AttendeeScribe; +import biweekly.io.scribe.property.AudioAlarmScribe; +import biweekly.io.scribe.property.CalendarScaleScribe; +import biweekly.io.scribe.property.CategoriesScribe; +import biweekly.io.scribe.property.ClassificationScribe; +import biweekly.io.scribe.property.ColorScribe; +import biweekly.io.scribe.property.CommentScribe; +import biweekly.io.scribe.property.CompletedScribe; +import biweekly.io.scribe.property.ConferenceScribe; +import biweekly.io.scribe.property.ContactScribe; +import biweekly.io.scribe.property.CreatedScribe; +import biweekly.io.scribe.property.DateDueScribe; +import biweekly.io.scribe.property.DateEndScribe; +import biweekly.io.scribe.property.DateStartScribe; +import biweekly.io.scribe.property.DateTimeStampScribe; +import biweekly.io.scribe.property.DaylightScribe; +import biweekly.io.scribe.property.DescriptionScribe; +import biweekly.io.scribe.property.DisplayAlarmScribe; +import biweekly.io.scribe.property.DurationPropertyScribe; +import biweekly.io.scribe.property.EmailAlarmScribe; +import biweekly.io.scribe.property.ExceptionDatesScribe; +import biweekly.io.scribe.property.ExceptionRuleScribe; +import biweekly.io.scribe.property.FreeBusyScribe; +import biweekly.io.scribe.property.GeoScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.io.scribe.property.ImageScribe; +import biweekly.io.scribe.property.LastModifiedScribe; +import biweekly.io.scribe.property.LocationScribe; +import biweekly.io.scribe.property.MethodScribe; +import biweekly.io.scribe.property.NameScribe; +import biweekly.io.scribe.property.OrganizerScribe; +import biweekly.io.scribe.property.PercentCompleteScribe; +import biweekly.io.scribe.property.PriorityScribe; +import biweekly.io.scribe.property.ProcedureAlarmScribe; +import biweekly.io.scribe.property.ProductIdScribe; +import biweekly.io.scribe.property.RawPropertyScribe; +import biweekly.io.scribe.property.RecurrenceDatesScribe; +import biweekly.io.scribe.property.RecurrenceIdScribe; +import biweekly.io.scribe.property.RecurrenceRuleScribe; +import biweekly.io.scribe.property.RefreshIntervalScribe; +import biweekly.io.scribe.property.RelatedToScribe; +import biweekly.io.scribe.property.RepeatScribe; +import biweekly.io.scribe.property.RequestStatusScribe; +import biweekly.io.scribe.property.ResourcesScribe; +import biweekly.io.scribe.property.SequenceScribe; +import biweekly.io.scribe.property.SourceScribe; +import biweekly.io.scribe.property.StatusScribe; +import biweekly.io.scribe.property.SummaryScribe; +import biweekly.io.scribe.property.TimezoneIdScribe; +import biweekly.io.scribe.property.TimezoneNameScribe; +import biweekly.io.scribe.property.TimezoneOffsetFromScribe; +import biweekly.io.scribe.property.TimezoneOffsetToScribe; +import biweekly.io.scribe.property.TimezoneScribe; +import biweekly.io.scribe.property.TimezoneUrlScribe; +import biweekly.io.scribe.property.TransparencyScribe; +import biweekly.io.scribe.property.TriggerScribe; +import biweekly.io.scribe.property.UidScribe; +import biweekly.io.scribe.property.UrlScribe; +import biweekly.io.scribe.property.VersionScribe; +import biweekly.io.scribe.property.XmlScribe; +import biweekly.io.xml.XCalNamespaceContext; +import biweekly.property.ICalProperty; +import biweekly.property.RawProperty; +import biweekly.property.Xml; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Manages a listing of component and property scribes. This is useful for + * injecting the scribes of any experimental components or properties you have + * defined into a reader or writer object. The same ScribeIndex instance can be + * reused and injected into multiple reader/writer classes. + *

+ *

+ * Example: + *

+ * + *
+ * //init the index
+ * ScribeIndex index = new ScribeIndex();
+ * index.register(new CustomPropertyScribe());
+ * index.register(new AnotherCustomPropertyScribe());
+ * index.register(new CustomComponentScribe());
+ * 
+ * //inject into a reader class
+ * ICalReader reader = new ICalReader(...);
+ * reader.setScribeIndex(index);
+ * List<ICalendar> icals = new ArrayList<ICalendar>();
+ * ICalendar ical;
+ * while ((ical = reader.readNext()) != null) {
+ *   icals.add(ical);
+ * }
+ * 
+ * //inject the same instance in another reader/writer class
+ * JCalWriter writer = new JCalWriter(...);
+ * writer.setScribeIndex(index);
+ * for (ICalendar ical : icals) {
+ *   writer.write(ical);
+ * }
+ * 
+ * @author Michael Angstadt + */ +public class ScribeIndex { + //define standard component scribes + private static final Map> standardCompByName = new HashMap>(); + private static final Map, ICalComponentScribe> standardCompByClass = new HashMap, ICalComponentScribe>(); + static { + registerStandard(new ICalendarScribe()); + registerStandard(new VAlarmScribe()); + registerStandard(new VEventScribe()); + registerStandard(new VFreeBusyScribe()); + registerStandard(new VJournalScribe()); + registerStandard(new VTodoScribe()); + registerStandard(new VTimezoneScribe()); + registerStandard(new StandardTimeScribe()); + registerStandard(new DaylightSavingsTimeScribe()); + } + + //define standard property scribes + private static final Map> standardPropByName = new HashMap>(); + private static final Map, ICalPropertyScribe> standardPropByClass = new HashMap, ICalPropertyScribe>(); + private static final Map> standardPropByQName = new HashMap>(); + static { + //RFC 5545 + registerStandard(new ActionScribe()); + registerStandard(new AttachmentScribe()); + registerStandard(new AttendeeScribe()); + registerStandard(new CalendarScaleScribe()); + registerStandard(new CategoriesScribe()); + registerStandard(new ClassificationScribe()); + registerStandard(new CommentScribe()); + registerStandard(new CompletedScribe()); + registerStandard(new ContactScribe()); + registerStandard(new CreatedScribe()); + registerStandard(new DateDueScribe()); + registerStandard(new DateEndScribe()); + registerStandard(new DateStartScribe()); + registerStandard(new DateTimeStampScribe()); + registerStandard(new DescriptionScribe()); + registerStandard(new DurationPropertyScribe()); + registerStandard(new ExceptionDatesScribe()); + registerStandard(new FreeBusyScribe()); + registerStandard(new GeoScribe()); + registerStandard(new LastModifiedScribe()); + registerStandard(new LocationScribe()); + registerStandard(new MethodScribe()); + registerStandard(new OrganizerScribe()); + registerStandard(new PercentCompleteScribe()); + registerStandard(new PriorityScribe()); + registerStandard(new ProductIdScribe()); + registerStandard(new RecurrenceDatesScribe()); + registerStandard(new RecurrenceIdScribe()); + registerStandard(new RecurrenceRuleScribe()); + registerStandard(new RelatedToScribe()); + registerStandard(new RepeatScribe()); + registerStandard(new RequestStatusScribe()); + registerStandard(new ResourcesScribe()); + registerStandard(new SequenceScribe()); + registerStandard(new StatusScribe()); + registerStandard(new SummaryScribe()); + registerStandard(new TimezoneIdScribe()); + registerStandard(new TimezoneNameScribe()); + registerStandard(new TimezoneOffsetFromScribe()); + registerStandard(new TimezoneOffsetToScribe()); + registerStandard(new TimezoneUrlScribe()); + registerStandard(new TransparencyScribe()); + registerStandard(new TriggerScribe()); + registerStandard(new UidScribe()); + registerStandard(new UrlScribe()); + registerStandard(new VersionScribe()); + + //RFC 6321 + registerStandard(new XmlScribe()); + + //RFC 2445 + registerStandard(new ExceptionRuleScribe()); + + //vCal + registerStandard(new AudioAlarmScribe()); + registerStandard(new DaylightScribe()); + registerStandard(new DisplayAlarmScribe()); + registerStandard(new EmailAlarmScribe()); + registerStandard(new ProcedureAlarmScribe()); + registerStandard(new TimezoneScribe()); + + //draft-ietf-calext-extensions-01 + registerStandard(new ColorScribe()); + registerStandard(new ConferenceScribe()); + registerStandard(new ImageScribe()); + registerStandard(new NameScribe()); + registerStandard(new SourceScribe()); + registerStandard(new RefreshIntervalScribe()); + } + + private final Map> experimentalCompByName = new HashMap>(0); + private final Map, ICalComponentScribe> experimentalCompByClass = new HashMap, ICalComponentScribe>(0); + + private final Map> experimentalPropByName = new HashMap>(0); + private final Map, ICalPropertyScribe> experimentalPropByClass = new HashMap, ICalPropertyScribe>(0); + private final Map> experimentalPropByQName = new HashMap>(0); + + /** + * Gets a component scribe by name. + * @param componentName the component name (e.g. "VEVENT") + * @param version the version of the iCalendar object being parsed + * @return the component scribe or a {@link RawComponentScribe} if not found + */ + public ICalComponentScribe getComponentScribe(String componentName, ICalVersion version) { + componentName = componentName.toUpperCase(); + + ICalComponentScribe scribe = experimentalCompByName.get(componentName); + if (scribe == null) { + scribe = standardCompByName.get(componentName); + } + + if (scribe == null) { + return new RawComponentScribe(componentName); + } + + if (version != null && !scribe.getSupportedVersions().contains(version)) { + //treat the component as a raw component if the current iCal version doesn't support it + return new RawComponentScribe(componentName); + } + + return scribe; + } + + /** + * Gets a property scribe by name. + * @param propertyName the property name (e.g. "UID") + * @param version the version of the iCalendar object being parsed + * @return the property scribe or a {@link RawPropertyScribe} if not found + */ + public ICalPropertyScribe getPropertyScribe(String propertyName, ICalVersion version) { + propertyName = propertyName.toUpperCase(); + + String key = propertyNameKey(propertyName, version); + ICalPropertyScribe scribe = experimentalPropByName.get(key); + if (scribe == null) { + scribe = standardPropByName.get(key); + } + + if (scribe == null) { + return new RawPropertyScribe(propertyName); + } + + if (version != null && !scribe.getSupportedVersions().contains(version)) { + //treat the property as a raw property if the current iCal version doesn't support it + return new RawPropertyScribe(propertyName); + } + + return scribe; + } + + /** + * Gets a component scribe by class. + * @param clazz the component class + * @return the component scribe or null if not found + */ + public ICalComponentScribe getComponentScribe(Class clazz) { + ICalComponentScribe scribe = experimentalCompByClass.get(clazz); + if (scribe != null) { + return scribe; + } + + return standardCompByClass.get(clazz); + } + + /** + * Gets a property scribe by class. + * @param clazz the property class + * @return the property scribe or null if not found + */ + public ICalPropertyScribe getPropertyScribe(Class clazz) { + ICalPropertyScribe scribe = experimentalPropByClass.get(clazz); + if (scribe != null) { + return scribe; + } + + return standardPropByClass.get(clazz); + } + + /** + * Gets the appropriate component scribe for a given component instance. + * @param component the component instance + * @return the component scribe or null if not found + */ + public ICalComponentScribe getComponentScribe(ICalComponent component) { + if (component instanceof RawComponent) { + RawComponent raw = (RawComponent) component; + return new RawComponentScribe(raw.getName()); + } + + return getComponentScribe(component.getClass()); + } + + /** + * Gets the appropriate property scribe for a given property instance. + * @param property the property instance + * @return the property scribe or null if not found + */ + public ICalPropertyScribe getPropertyScribe(ICalProperty property) { + if (property instanceof RawProperty) { + RawProperty raw = (RawProperty) property; + return new RawPropertyScribe(raw.getName()); + } + + return getPropertyScribe(property.getClass()); + } + + /** + * Gets a property scribe by XML local name and namespace. + * @param qname the XML local name and namespace + * @return the property scribe or a {@link XmlScribe} if not found + */ + public ICalPropertyScribe getPropertyScribe(QName qname) { + ICalPropertyScribe scribe = experimentalPropByQName.get(qname); + if (scribe == null) { + scribe = standardPropByQName.get(qname); + } + + if (scribe == null || !scribe.getSupportedVersions().contains(ICalVersion.V2_0)) { + if (XCalNamespaceContext.XCAL_NS.equals(qname.getNamespaceURI())) { + return new RawPropertyScribe(qname.getLocalPart().toUpperCase()); + } + return getPropertyScribe(Xml.class); + } + + return scribe; + } + + /** + * Registers a component scribe. + * @param scribe the scribe to register + */ + public void register(ICalComponentScribe scribe) { + experimentalCompByName.put(scribe.getComponentName().toUpperCase(), scribe); + experimentalCompByClass.put(scribe.getComponentClass(), scribe); + } + + /** + * Registers a property scribe. + * @param scribe the scribe to register + */ + public void register(ICalPropertyScribe scribe) { + for (ICalVersion version : ICalVersion.values()) { + experimentalPropByName.put(propertyNameKey(scribe, version), scribe); + } + experimentalPropByClass.put(scribe.getPropertyClass(), scribe); + experimentalPropByQName.put(scribe.getQName(), scribe); + } + + /** + * Unregisters a component scribe. + * @param scribe the scribe to unregister + */ + public void unregister(ICalComponentScribe scribe) { + experimentalCompByName.remove(scribe.getComponentName().toUpperCase()); + experimentalCompByClass.remove(scribe.getComponentClass()); + } + + /** + * Unregisters a property scribe + * @param scribe the scribe to unregister + */ + public void unregister(ICalPropertyScribe scribe) { + for (ICalVersion version : ICalVersion.values()) { + experimentalPropByName.remove(propertyNameKey(scribe, version)); + } + experimentalPropByClass.remove(scribe.getPropertyClass()); + experimentalPropByQName.remove(scribe.getQName()); + } + + /** + * Convenience method for getting the scribe of the root iCalendar component + * ("VCALENDAR"). + * @return the scribe + */ + public static ICalendarScribe getICalendarScribe() { + return (ICalendarScribe) standardCompByClass.get(ICalendar.class); + } + + private static void registerStandard(ICalComponentScribe scribe) { + standardCompByName.put(scribe.getComponentName().toUpperCase(), scribe); + standardCompByClass.put(scribe.getComponentClass(), scribe); + } + + private static void registerStandard(ICalPropertyScribe scribe) { + for (ICalVersion version : ICalVersion.values()) { + standardPropByName.put(propertyNameKey(scribe, version), scribe); + } + standardPropByClass.put(scribe.getPropertyClass(), scribe); + standardPropByQName.put(scribe.getQName(), scribe); + } + + private static String propertyNameKey(ICalPropertyScribe scribe, ICalVersion version) { + return propertyNameKey(scribe.getPropertyName(version), version); + } + + private static String propertyNameKey(String propertyName, ICalVersion version) { + return version.ordinal() + propertyName.toUpperCase(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/DaylightSavingsTimeScribe.java b/app/src/main/java/biweekly/io/scribe/component/DaylightSavingsTimeScribe.java new file mode 100644 index 0000000000..0be1f3c047 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/DaylightSavingsTimeScribe.java @@ -0,0 +1,42 @@ +package biweekly.io.scribe.component; + +import biweekly.component.DaylightSavingsTime; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class DaylightSavingsTimeScribe extends ObservanceScribe { + public DaylightSavingsTimeScribe() { + super(DaylightSavingsTime.class, "DAYLIGHT"); + } + + @Override + protected DaylightSavingsTime _newInstance() { + return new DaylightSavingsTime(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/ICalComponentScribe.java b/app/src/main/java/biweekly/io/scribe/component/ICalComponentScribe.java new file mode 100644 index 0000000000..f6d88309b4 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/ICalComponentScribe.java @@ -0,0 +1,146 @@ +package biweekly.io.scribe.component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.component.ICalComponent; +import biweekly.io.DataModelConversionException; +import biweekly.property.ICalProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Base class for iCalendar component scribes. + * @param the component class + * @author Michael Angstadt + */ +public abstract class ICalComponentScribe { + private static final Set allVersions = Collections.unmodifiableSet(EnumSet.allOf(ICalVersion.class)); + + protected final Class clazz; + protected final String componentName; + + /** + * Creates a new component scribe. + * @param clazz the component's class + * @param componentName the component's name (e.g. "VEVENT") + */ + public ICalComponentScribe(Class clazz, String componentName) { + this.clazz = clazz; + this.componentName = componentName; + } + + /** + * Gets the iCalendar versions that support this component. This method + * returns all iCalendar versions unless overridden by the child scribe. + * @return the iCalendar versions + */ + public Set getSupportedVersions() { + return allVersions; + } + + /** + * Gets the component class. + * @return the component class. + */ + public Class getComponentClass() { + return clazz; + } + + /** + * Gets the component's name. + * @return the compent's name (e.g. "VEVENT") + */ + public String getComponentName() { + return componentName; + } + + /** + * Creates a new instance of the component class that doesn't have any + * properties or sub-components. + * @return the new instance + */ + public T emptyInstance() { + T component = _newInstance(); + + //remove any properties/components that were created in the constructor + component.getProperties().clear(); + component.getComponents().clear(); + + return component; + } + + /** + * Creates a new instance of the component class. + * @return the new instance + */ + protected abstract T _newInstance(); + + /** + * Gets the sub-components to marshal. Child classes can override this for + * better control over which components are marshalled. + * @param component the component + * @return the sub-components to marshal + */ + public List getComponents(T component) { + return new ArrayList(component.getComponents().values()); + } + + /** + * Gets the properties to marshal. Child classes can override this for + * better control over which properties are marshalled. + * @param component the component + * @return the properties to marshal + */ + public List getProperties(T component) { + return new ArrayList(component.getProperties().values()); + } + + /** + *

+ * Checks this component to see if it needs to be converted to a different + * data model before writing it out, throwing a + * {@link DataModelConversionException} if it does. + *

+ *

+ * Child classes should override this method if the component requires + * any such conversion. The default implementation of this method does + * nothing. + *

+ * @param component the component being written + * @param parent the component's parent or null if it has no parent + * @param version the version iCalendar object being written + * @throws DataModelConversionException if the component needs to be + * converted + */ + public void checkForDataModelConversions(T component, ICalComponent parent, ICalVersion version) { + //empty + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/ICalendarScribe.java b/app/src/main/java/biweekly/io/scribe/component/ICalendarScribe.java new file mode 100644 index 0000000000..e9a946417c --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/ICalendarScribe.java @@ -0,0 +1,70 @@ +package biweekly.io.scribe.component; + +import java.util.ArrayList; +import java.util.List; + +import biweekly.ICalendar; +import biweekly.property.ICalProperty; +import biweekly.property.ProductId; +import biweekly.property.Version; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class ICalendarScribe extends ICalComponentScribe { + public ICalendarScribe() { + super(ICalendar.class, "VCALENDAR"); + } + + @Override + protected ICalendar _newInstance() { + return new ICalendar(); + } + + @Override + public List getProperties(ICalendar component) { + List properties = new ArrayList(component.getProperties().values()); + + /* + * Move VERSION properties to the front (if any are present), followed + * by PRODID properties. This is not required by the specs, but may help + * with interoperability because all the examples in the specs put the + * VERSION and PRODID at the very beginning of the iCalendar. + */ + moveToFront(ProductId.class, component, properties); + moveToFront(Version.class, component, properties); + + return properties; + } + + private void moveToFront(Class clazz, ICalendar component, List properties) { + List toMove = component.getProperties(clazz); + properties.removeAll(toMove); + properties.addAll(0, toMove); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/ObservanceScribe.java b/app/src/main/java/biweekly/io/scribe/component/ObservanceScribe.java new file mode 100644 index 0000000000..3911cc51c0 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/ObservanceScribe.java @@ -0,0 +1,46 @@ +package biweekly.io.scribe.component; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.component.Observance; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public abstract class ObservanceScribe extends ICalComponentScribe { + protected ObservanceScribe(Class clazz, String componentName) { + super(clazz, componentName); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/RawComponentScribe.java b/app/src/main/java/biweekly/io/scribe/component/RawComponentScribe.java new file mode 100644 index 0000000000..266e67f2cf --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/RawComponentScribe.java @@ -0,0 +1,46 @@ +package biweekly.io.scribe.component; + +import biweekly.component.RawComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class RawComponentScribe extends ICalComponentScribe { + /** + * Creates a new raw component scribe. + * @param componentName the component's name (e.g. "X-PARTY") + */ + public RawComponentScribe(String componentName) { + super(RawComponent.class, componentName); + } + + @Override + protected RawComponent _newInstance() { + return new RawComponent(componentName); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/StandardTimeScribe.java b/app/src/main/java/biweekly/io/scribe/component/StandardTimeScribe.java new file mode 100644 index 0000000000..5acdfb2053 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/StandardTimeScribe.java @@ -0,0 +1,42 @@ +package biweekly.io.scribe.component; + +import biweekly.component.StandardTime; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class StandardTimeScribe extends ObservanceScribe { + public StandardTimeScribe() { + super(StandardTime.class, "STANDARD"); + } + + @Override + protected StandardTime _newInstance() { + return new StandardTime(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/VAlarmScribe.java b/app/src/main/java/biweekly/io/scribe/component/VAlarmScribe.java new file mode 100644 index 0000000000..1b5bb31bbb --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/VAlarmScribe.java @@ -0,0 +1,230 @@ +package biweekly.io.scribe.component; + +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.component.ICalComponent; +import biweekly.component.VAlarm; +import biweekly.io.DataModelConversionException; +import biweekly.parameter.Related; +import biweekly.property.Action; +import biweekly.property.Attachment; +import biweekly.property.Attendee; +import biweekly.property.AudioAlarm; +import biweekly.property.DateEnd; +import biweekly.property.DateStart; +import biweekly.property.Description; +import biweekly.property.DisplayAlarm; +import biweekly.property.DurationProperty; +import biweekly.property.EmailAlarm; +import biweekly.property.ProcedureAlarm; +import biweekly.property.Repeat; +import biweekly.property.Trigger; +import biweekly.property.VCalAlarmProperty; +import biweekly.property.ValuedProperty; +import biweekly.util.Duration; +import biweekly.util.StringUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class VAlarmScribe extends ICalComponentScribe { + public VAlarmScribe() { + super(VAlarm.class, "VALARM"); + } + + @Override + protected VAlarm _newInstance() { + return new VAlarm(null, null); + } + + @Override + public void checkForDataModelConversions(VAlarm component, ICalComponent parent, ICalVersion version) { + if (version != ICalVersion.V1_0) { + return; + } + + VCalAlarmProperty vcalAlarm = convert(component, parent); + if (vcalAlarm == null) { + return; + } + + DataModelConversionException e = new DataModelConversionException(null); + e.getProperties().add(vcalAlarm); + throw e; + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } + + /** + * Converts a {@link VAlarm} component to a vCal alarm property. + * @param valarm the component + * @param parent the component's parent + * @return the vCal alarm property or null if it cannot be converted + */ + private static VCalAlarmProperty convert(VAlarm valarm, ICalComponent parent) { + VCalAlarmProperty property = create(valarm); + if (property == null) { + return null; + } + + property.setStart(determineStartDate(valarm, parent)); + + DurationProperty duration = valarm.getDuration(); + if (duration != null) { + property.setSnooze(duration.getValue()); + } + + Repeat repeat = valarm.getRepeat(); + if (repeat != null) { + property.setRepeat(repeat.getValue()); + } + + return property; + } + + /** + * Creates a new {@link VCalAlarmProperty} based on the given {@link VAlarm} + * component, setting fields that are common to all + * {@link VCalAlarmProperty} classes. + * @param valarm the source component + * @return the property or null if it cannot be created + */ + private static VCalAlarmProperty create(VAlarm valarm) { + Action action = valarm.getAction(); + if (action == null) { + return null; + } + + if (action.isAudio()) { + AudioAlarm aalarm = new AudioAlarm(); + + List attaches = valarm.getAttachments(); + if (!attaches.isEmpty()) { + Attachment attach = attaches.get(0); + + String formatType = attach.getFormatType(); + aalarm.setParameter("TYPE", formatType); + + byte[] data = attach.getData(); + if (data != null) { + aalarm.setData(data); + } + + String uri = attach.getUri(); + if (uri != null) { + String contentId = StringUtils.afterPrefixIgnoreCase(uri, "cid:"); + if (contentId == null) { + aalarm.setUri(uri); + } else { + aalarm.setContentId(contentId); + } + } + } + + return aalarm; + } + + if (action.isDisplay()) { + Description description = valarm.getDescription(); + String text = ValuedProperty.getValue(description); + return new DisplayAlarm(text); + } + + if (action.isEmail()) { + List attendees = valarm.getAttendees(); + String email = attendees.isEmpty() ? null : attendees.get(0).getEmail(); + EmailAlarm malarm = new EmailAlarm(email); + + Description description = valarm.getDescription(); + String note = ValuedProperty.getValue(description); + malarm.setNote(note); + + return malarm; + } + + if (action.isProcedure()) { + Description description = valarm.getDescription(); + String path = ValuedProperty.getValue(description); + return new ProcedureAlarm(path); + } + + return null; + } + + /** + * Determines what the alarm property's start date should be. + * @param valarm the component that is being converted to a vCal alarm + * property + * @param parent the component's parent + * @return the start date or null if it cannot be determined + */ + private static Date determineStartDate(VAlarm valarm, ICalComponent parent) { + Trigger trigger = valarm.getTrigger(); + if (trigger == null) { + return null; + } + + Date triggerStart = trigger.getDate(); + if (triggerStart != null) { + return triggerStart; + } + + Duration triggerDuration = trigger.getDuration(); + if (triggerDuration == null) { + return null; + } + + if (parent == null) { + return null; + } + + Related related = trigger.getRelated(); + Date date = null; + if (related == Related.START) { + date = ValuedProperty.getValue(parent.getProperty(DateStart.class)); + } else if (related == Related.END) { + date = ValuedProperty.getValue(parent.getProperty(DateEnd.class)); + if (date == null) { + Date dateStart = ValuedProperty.getValue(parent.getProperty(DateStart.class)); + Duration duration = ValuedProperty.getValue(parent.getProperty(DurationProperty.class)); + if (duration != null && dateStart != null) { + date = duration.add(dateStart); + } + } + } + + return (date == null) ? null : triggerDuration.add(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/VEventScribe.java b/app/src/main/java/biweekly/io/scribe/component/VEventScribe.java new file mode 100644 index 0000000000..9c64e34a81 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/VEventScribe.java @@ -0,0 +1,42 @@ +package biweekly.io.scribe.component; + +import biweekly.component.VEvent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class VEventScribe extends ICalComponentScribe { + public VEventScribe() { + super(VEvent.class, "VEVENT"); + } + + @Override + protected VEvent _newInstance() { + return new VEvent(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/VFreeBusyScribe.java b/app/src/main/java/biweekly/io/scribe/component/VFreeBusyScribe.java new file mode 100644 index 0000000000..a974a4e894 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/VFreeBusyScribe.java @@ -0,0 +1,118 @@ +package biweekly.io.scribe.component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.component.VFreeBusy; +import biweekly.property.FreeBusy; +import biweekly.property.ICalProperty; +import biweekly.util.Period; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class VFreeBusyScribe extends ICalComponentScribe { + public VFreeBusyScribe() { + super(VFreeBusy.class, "VFREEBUSY"); + } + + @Override + public List getProperties(VFreeBusy component) { + List properties = super.getProperties(component); + + List fb = new ArrayList(component.getFreeBusy()); + if (fb.isEmpty()) { + return properties; + } + + //sort FREEBUSY properties by start date (p.100) + Collections.sort(fb, new Comparator() { + public int compare(FreeBusy one, FreeBusy two) { + Date oneStart = getEarliestStartDate(one); + Date twoStart = getEarliestStartDate(two); + if (oneStart == null && twoStart == null) { + return 0; + } + if (oneStart == null) { + return 1; + } + if (twoStart == null) { + return -1; + } + return oneStart.compareTo(twoStart); + } + + private Date getEarliestStartDate(FreeBusy fb) { + Date date = null; + for (Period tp : fb.getValues()) { + if (tp.getStartDate() == null) { + continue; + } + if (date == null || date.compareTo(tp.getStartDate()) > 0) { + date = tp.getStartDate(); + } + } + return date; + } + }); + + //find index of first FREEBUSY instance + int index = 0; + for (ICalProperty prop : properties) { + if (prop instanceof FreeBusy) { + break; + } + index++; + } + + //remove and re-add the FREEBUSY instances in sorted order + properties = new ArrayList(properties); + for (FreeBusy f : fb) { + properties.remove(f); + properties.add(index++, f); + } + + return properties; + } + + @Override + protected VFreeBusy _newInstance() { + return new VFreeBusy(); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/VJournalScribe.java b/app/src/main/java/biweekly/io/scribe/component/VJournalScribe.java new file mode 100644 index 0000000000..57722bfdc3 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/VJournalScribe.java @@ -0,0 +1,51 @@ +package biweekly.io.scribe.component; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.component.VJournal; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class VJournalScribe extends ICalComponentScribe { + public VJournalScribe() { + super(VJournal.class, "VJOURNAL"); + } + + @Override + protected VJournal _newInstance() { + return new VJournal(); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/VTimezoneScribe.java b/app/src/main/java/biweekly/io/scribe/component/VTimezoneScribe.java new file mode 100644 index 0000000000..b79c0b2f98 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/VTimezoneScribe.java @@ -0,0 +1,51 @@ +package biweekly.io.scribe.component; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.component.VTimezone; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class VTimezoneScribe extends ICalComponentScribe { + public VTimezoneScribe() { + super(VTimezone.class, "VTIMEZONE"); + } + + @Override + protected VTimezone _newInstance() { + return new VTimezone((String) null); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/VTodoScribe.java b/app/src/main/java/biweekly/io/scribe/component/VTodoScribe.java new file mode 100644 index 0000000000..ed1e6e00de --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/VTodoScribe.java @@ -0,0 +1,42 @@ +package biweekly.io.scribe.component; + +import biweekly.component.VTodo; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @author Michael Angstadt + */ +public class VTodoScribe extends ICalComponentScribe { + public VTodoScribe() { + super(VTodo.class, "VTODO"); + } + + @Override + protected VTodo _newInstance() { + return new VTodo(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/component/package-info.java b/app/src/main/java/biweekly/io/scribe/component/package-info.java new file mode 100644 index 0000000000..b15a354ee1 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/component/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes that marshal and unmarshal components. + */ +package biweekly.io.scribe.component; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/package-info.java b/app/src/main/java/biweekly/io/scribe/package-info.java new file mode 100644 index 0000000000..b370957b0d --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes that marshal and unmarshal components and properties in various formats. + */ +package biweekly.io.scribe; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/ActionScribe.java b/app/src/main/java/biweekly/io/scribe/property/ActionScribe.java new file mode 100644 index 0000000000..28827c8788 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ActionScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.Action; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Action} properties. + * @author Michael Angstadt + */ +public class ActionScribe extends TextPropertyScribe { + public ActionScribe() { + super(Action.class, "ACTION"); + } + + @Override + protected Action newInstance(String value, ICalVersion version) { + return new Action(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/AttachmentScribe.java b/app/src/main/java/biweekly/io/scribe/property/AttachmentScribe.java new file mode 100644 index 0000000000..4b9799f6ff --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/AttachmentScribe.java @@ -0,0 +1,137 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.property.Attachment; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Attachment} properties. + * @author Michael Angstadt + */ +public class AttachmentScribe extends BinaryPropertyScribe { + public AttachmentScribe() { + super(Attachment.class, "ATTACH"); + } + + @Override + protected ICalDataType _dataType(Attachment property, ICalVersion version) { + if (property.getContentId() != null) { + return (version == ICalVersion.V1_0) ? ICalDataType.CONTENT_ID : ICalDataType.URI; + } + return super._dataType(property, version); + } + + @Override + protected Attachment newInstance(byte[] data) { + /* + * Note: "formatType" will be set when the parameters are assigned to + * the property object. + */ + return new Attachment(null, data); + } + + @Override + protected Attachment newInstance(String value, ICalDataType dataType) { + /* + * Note: "formatType" will be set when the parameters are assigned to + * the property object. + */ + + if (dataType == ICalDataType.CONTENT_ID) { + String contentId = getCidUriValue(value); + if (contentId == null) { + contentId = value; + } + Attachment attach = new Attachment(null, (String) null); + attach.setContentId(contentId); + return attach; + } + + String contentId = getCidUriValue(value); + if (contentId != null) { + Attachment attach = new Attachment(null, (String) null); + attach.setContentId(contentId); + return attach; + } + + return new Attachment(null, value); + } + + @Override + protected String _writeText(Attachment property, WriteContext context) { + String contentId = property.getContentId(); + if (contentId != null) { + return (context.getVersion() == ICalVersion.V1_0) ? '<' + contentId + '>' : "cid:" + contentId; + } + + return super._writeText(property, context); + } + + @Override + protected void _writeXml(Attachment property, XCalElement element, WriteContext context) { + String contentId = property.getContentId(); + if (contentId != null) { + element.append(ICalDataType.URI, "cid:" + contentId); + return; + } + + super._writeXml(property, element, context); + } + + @Override + protected JCalValue _writeJson(Attachment property, WriteContext context) { + String contentId = property.getContentId(); + if (contentId != null) { + return JCalValue.single("cid:" + contentId); + } + + return super._writeJson(property, context); + } + + /** + * Gets the value of the given "cid" URI. + * @param uri the "cid" URI + * @return the URI value or null if the given string is not a "cid" URI + */ + private static String getCidUriValue(String uri) { + int colon = uri.indexOf(':'); + if (colon == 3) { + String scheme = uri.substring(0, colon); + return "cid".equalsIgnoreCase(scheme) ? uri.substring(colon + 1) : null; + } + + if (uri.length() > 0 && uri.charAt(0) == '<' && uri.charAt(uri.length() - 1) == '>') { + return uri.substring(1, uri.length() - 1); + } + + return null; + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/AttendeeScribe.java b/app/src/main/java/biweekly/io/scribe/property/AttendeeScribe.java new file mode 100644 index 0000000000..a6b503cc45 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/AttendeeScribe.java @@ -0,0 +1,340 @@ +package biweekly.io.scribe.property; + +import java.util.Iterator; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.DataModelConversionException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.parameter.ICalParameters; +import biweekly.parameter.ParticipationLevel; +import biweekly.parameter.ParticipationStatus; +import biweekly.parameter.Role; +import biweekly.property.Attendee; +import biweekly.property.Organizer; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Attendee} properties. + * @author Michael Angstadt + */ +public class AttendeeScribe extends ICalPropertyScribe { + public AttendeeScribe() { + super(Attendee.class, "ATTENDEE"); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + switch (version) { + case V1_0: + return null; + default: + return ICalDataType.CAL_ADDRESS; + } + } + + @Override + protected ICalDataType _dataType(Attendee property, ICalVersion version) { + if (version == ICalVersion.V1_0 && property.getUri() != null) { + return ICalDataType.URL; + } + return defaultDataType(version); + } + + @Override + protected ICalParameters _prepareParameters(Attendee property, WriteContext context) { + /* + * Note: Parameter values are assigned using "put()" instead of the + * appropriate "setter" methods so that any existing parameter values + * are not overwritten. + */ + + ICalParameters copy = new ICalParameters(property.getParameters()); + + //RSVP parameter + //1.0 - Uses the values "YES" and "NO" + //2.0 - Uses the values "TRUE" and "FALSE" + Boolean rsvp = property.getRsvp(); + if (rsvp != null) { + String value; + switch (context.getVersion()) { + case V1_0: + value = rsvp ? "YES" : "NO"; + break; + + default: + value = rsvp ? "TRUE" : "FALSE"; + break; + } + + copy.put(ICalParameters.RSVP, value); + } + + //ROLE and EXPECT parameters + //1.0 - Uses ROLE and EXPECT + //2.0 - Uses only ROLE + Role role = property.getRole(); + ParticipationLevel level = property.getParticipationLevel(); + switch (context.getVersion()) { + case V1_0: + if (role != null) { + copy.put(ICalParameters.ROLE, role.getValue()); + } + if (level != null) { + copy.put(ICalParameters.EXPECT, level.getValue(context.getVersion())); + } + break; + + default: + String value = null; + if (role == Role.CHAIR) { + value = role.getValue(); + } else if (level != null) { + value = level.getValue(context.getVersion()); + } else if (role != null) { + value = role.getValue(); + } + + if (value != null) { + copy.put(ICalParameters.ROLE, value); + } + break; + } + + //PARTSTAT vs STATUS + //1.0 - Calls the parameter "STATUS" + //2.0 - Calls the parameter "PARTSTAT" + ParticipationStatus partStat = property.getParticipationStatus(); + if (partStat != null) { + String paramName; + String paramValue; + + switch (context.getVersion()) { + case V1_0: + paramName = ICalParameters.STATUS; + paramValue = (partStat == ParticipationStatus.NEEDS_ACTION) ? "NEEDS ACTION" : partStat.getValue(); + break; + + default: + paramName = ICalParameters.PARTSTAT; + paramValue = partStat.getValue(); + break; + } + + copy.put(paramName, paramValue); + } + + //CN parameter + String name = property.getCommonName(); + if (name != null && context.getVersion() != ICalVersion.V1_0) { + copy.put(ICalParameters.CN, name); + } + + //EMAIL parameter + String uri = property.getUri(); + String email = property.getEmail(); + if (uri != null && email != null && context.getVersion() != ICalVersion.V1_0) { + copy.put(ICalParameters.EMAIL, email); + } + + return copy; + } + + @Override + protected Attendee _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String uri = null, name = null, email = null; + Boolean rsvp = null; + Role role = null; + ParticipationLevel participationLevel = null; + ParticipationStatus participationStatus = null; + + switch (context.getVersion()) { + case V1_0: + Iterator it = parameters.get(ICalParameters.RSVP).iterator(); + while (it.hasNext()) { + String rsvpStr = it.next(); + + if ("YES".equalsIgnoreCase(rsvpStr)) { + rsvp = Boolean.TRUE; + it.remove(); + break; + } + + if ("NO".equalsIgnoreCase(rsvpStr)) { + rsvp = Boolean.FALSE; + it.remove(); + break; + } + } + + String roleStr = parameters.first(ICalParameters.ROLE); + if (roleStr != null) { + role = Role.get(roleStr); + parameters.remove(ICalParameters.ROLE, roleStr); + } + + String expectStr = parameters.getExpect(); + if (expectStr != null) { + participationLevel = ParticipationLevel.get(expectStr); + parameters.remove(ICalParameters.EXPECT, expectStr); + } + + String statusStr = parameters.getStatus(); + if (statusStr != null) { + participationStatus = ParticipationStatus.get(statusStr); + parameters.remove(ICalParameters.STATUS, statusStr); + } + + int bracketStart = value.lastIndexOf('<'); + int bracketEnd = value.lastIndexOf('>'); + if (bracketStart >= 0 && bracketEnd >= 0 && bracketStart < bracketEnd) { + name = value.substring(0, bracketStart).trim(); + email = value.substring(bracketStart + 1, bracketEnd).trim(); + } else if (dataType == ICalDataType.URL) { + uri = value; + } else { + email = value; + } + + break; + + default: + it = parameters.get(ICalParameters.RSVP).iterator(); + while (it.hasNext()) { + String rsvpStr = it.next(); + + if ("TRUE".equalsIgnoreCase(rsvpStr)) { + rsvp = Boolean.TRUE; + it.remove(); + break; + } + + if ("FALSE".equalsIgnoreCase(rsvpStr)) { + rsvp = Boolean.FALSE; + it.remove(); + break; + } + } + + roleStr = parameters.first(ICalParameters.ROLE); + if (roleStr != null) { + if (roleStr.equalsIgnoreCase(Role.CHAIR.getValue())) { + role = Role.CHAIR; + } else { + ParticipationLevel l = ParticipationLevel.find(roleStr); + if (l == null) { + role = Role.get(roleStr); + } else { + participationLevel = l; + } + } + parameters.remove(ICalParameters.ROLE, roleStr); + } + + String participationStatusStr = parameters.getParticipationStatus(); + if (participationStatusStr != null) { + participationStatus = ParticipationStatus.get(participationStatusStr); + parameters.remove(ICalParameters.PARTSTAT, participationStatusStr); + } + + name = parameters.getCommonName(); + if (name != null) { + parameters.remove(ICalParameters.CN, name); + } + + email = parameters.getEmail(); + if (email == null) { + int colon = value.indexOf(':'); + if (colon == 6) { + String scheme = value.substring(0, colon); + if (scheme.equalsIgnoreCase("mailto")) { + email = value.substring(colon + 1); + } else { + uri = value; + } + } else { + uri = value; + } + } else { + uri = value; + parameters.remove(ICalParameters.EMAIL, email); + } + + break; + } + + Attendee attendee = new Attendee(name, email, uri); + attendee.setParticipationStatus(participationStatus); + attendee.setParticipationLevel(participationLevel); + attendee.setRole(role); + attendee.setRsvp(rsvp); + + if (context.getVersion() == ICalVersion.V1_0 && attendee.getRole() == Role.ORGANIZER) { + Organizer organizer = new Organizer(attendee.getCommonName(), attendee.getEmail()); + organizer.setUri(attendee.getUri()); + organizer.setParameters(parameters); + + attendee.setParameters(parameters); + DataModelConversionException conversionException = new DataModelConversionException(attendee); + conversionException.getProperties().add(organizer); + throw conversionException; + } + + return attendee; + } + + @Override + protected String _writeText(Attendee property, WriteContext context) { + String uri = property.getUri(); + if (uri != null) { + return uri; + } + + String name = property.getCommonName(); + String email = property.getEmail(); + switch (context.getVersion()) { + case V1_0: + if (email != null) { + String value = (name == null) ? email : name + " <" + email + ">"; + return VObjectPropertyValues.escape(value); + } + + break; + + default: + if (email != null) { + return "mailto:" + email; + } + break; + } + + return ""; + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/AudioAlarmScribe.java b/app/src/main/java/biweekly/io/scribe/property/AudioAlarmScribe.java new file mode 100644 index 0000000000..4573dd8c08 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/AudioAlarmScribe.java @@ -0,0 +1,145 @@ +package biweekly.io.scribe.property; + +import java.util.Collections; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.component.VAlarm; +import biweekly.property.Action; +import biweekly.property.Attachment; +import biweekly.property.AudioAlarm; +import biweekly.util.org.apache.commons.codec.binary.Base64; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link AudioAlarm} properties. + * @author Michael Angstadt + */ +public class AudioAlarmScribe extends VCalAlarmPropertyScribe { + public AudioAlarmScribe() { + super(AudioAlarm.class, "AALARM"); + } + + @Override + protected ICalDataType _dataType(AudioAlarm property, ICalVersion version) { + if (property.getUri() != null) { + return ICalDataType.URL; + } + if (property.getData() != null) { + return ICalDataType.BINARY; + } + if (property.getContentId() != null) { + return ICalDataType.CONTENT_ID; + } + return null; + } + + @Override + protected List writeData(AudioAlarm property) { + String uri = property.getUri(); + if (uri != null) { + return Collections.singletonList(uri); + } + + byte[] data = property.getData(); + if (data != null) { + String base64Str = Base64.encodeBase64String(data); + return Collections.singletonList(base64Str); + } + + String contentId = property.getContentId(); + if (contentId != null) { + return Collections.singletonList(contentId); + } + + return Collections.emptyList(); + } + + @Override + protected AudioAlarm create(ICalDataType dataType, SemiStructuredValueIterator it) { + AudioAlarm aalarm = new AudioAlarm(); + String next = it.next(); + if (next == null) { + return aalarm; + } + + if (dataType == ICalDataType.BINARY) { + byte[] data = Base64.decodeBase64(next); + aalarm.setData(data); + } else if (dataType == ICalDataType.URL) { + aalarm.setUri(next); + } else if (dataType == ICalDataType.CONTENT_ID) { + aalarm.setContentId(next); + } else { + aalarm.setUri(next); + } + + return aalarm; + } + + @Override + protected void toVAlarm(VAlarm valarm, AudioAlarm property) { + Attachment attach = buildAttachment(property); + if (attach != null) { + valarm.addAttachment(attach); + } + } + + private static Attachment buildAttachment(AudioAlarm aalarm) { + String type = aalarm.getType(); + String contentType = (type == null) ? null : "audio/" + type.toLowerCase(); + Attachment attach = new Attachment(contentType, (String) null); + + byte[] data = aalarm.getData(); + if (data != null) { + attach.setData(data); + return attach; + } + + String contentId = aalarm.getContentId(); + if (contentId != null) { + attach.setContentId(contentId); + return attach; + } + + String uri = aalarm.getUri(); + if (uri != null) { + attach.setUri(uri); + return attach; + } + + return null; + } + + @Override + protected Action action() { + return Action.audio(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/BinaryPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/BinaryPropertyScribe.java new file mode 100644 index 0000000000..462b0a2295 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/BinaryPropertyScribe.java @@ -0,0 +1,175 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.Encoding; +import biweekly.parameter.ICalParameters; +import biweekly.property.BinaryProperty; +import biweekly.util.org.apache.commons.codec.binary.Base64; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link BinaryProperty} properties. + * @author Michael Angstadt + */ +public abstract class BinaryPropertyScribe extends ICalPropertyScribe { + public BinaryPropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName, ICalDataType.URI); + } + + @Override + protected ICalParameters _prepareParameters(T property, WriteContext context) { + ICalParameters copy = new ICalParameters(property.getParameters()); + + if (property.getUri() != null) { + copy.setEncoding(null); + } else if (property.getData() != null) { + copy.setEncoding(Encoding.BASE64); + } + + return copy; + } + + @Override + protected ICalDataType _dataType(T property, ICalVersion version) { + if (property.getUri() != null) { + return (version == ICalVersion.V1_0) ? ICalDataType.URL : ICalDataType.URI; + } + if (property.getData() != null) { + return ICalDataType.BINARY; + } + return defaultDataType(version); + } + + @Override + protected String _writeText(T property, WriteContext context) { + String uri = property.getUri(); + if (uri != null) { + return uri; + } + + byte[] data = property.getData(); + if (data != null) { + return Base64.encodeBase64String(data); + } + + return ""; + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + + if (dataType == ICalDataType.BINARY || parameters.getEncoding() == Encoding.BASE64) { + byte[] data = Base64.decodeBase64(value); + return newInstance(data); + } + + return newInstance(value, dataType); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + String uri = property.getUri(); + if (uri != null) { + element.append(ICalDataType.URI, uri); + return; + } + + byte[] data = property.getData(); + if (data != null) { + element.append(ICalDataType.BINARY, Base64.encodeBase64String(data)); + return; + } + + element.append(defaultDataType(context.getVersion()), ""); + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + String uri = element.first(ICalDataType.URI); + if (uri != null) { + return newInstance(uri, ICalDataType.URI); + } + + String base64Data = element.first(ICalDataType.BINARY); + if (base64Data != null) { + byte[] data = Base64.decodeBase64(base64Data); + return newInstance(data); + } + + throw missingXmlElements(ICalDataType.URI, ICalDataType.BINARY); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + String uri = property.getUri(); + if (uri != null) { + return JCalValue.single(uri); + } + + byte[] data = property.getData(); + if (data != null) { + return JCalValue.single(Base64.encodeBase64String(data)); + } + + return JCalValue.single(""); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = value.asSingle(); + + if (dataType == ICalDataType.BINARY) { + byte[] data = Base64.decodeBase64(valueStr); + return newInstance(data); + } + + return newInstance(valueStr, dataType); + } + + /** + * Creates a property object from the given binary data. + * @param data the data + * @return the property object + */ + protected abstract T newInstance(byte[] data); + + /** + * Creates a property object from the given string value. + * @param value the string value + * @param dataType the data type + * @return the property object + */ + protected abstract T newInstance(String value, ICalDataType dataType); +} diff --git a/app/src/main/java/biweekly/io/scribe/property/CalendarScaleScribe.java b/app/src/main/java/biweekly/io/scribe/property/CalendarScaleScribe.java new file mode 100644 index 0000000000..b96e8dc45b --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/CalendarScaleScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.CalendarScale; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link CalendarScale} properties. + * @author Michael Angstadt + */ +public class CalendarScaleScribe extends TextPropertyScribe { + public CalendarScaleScribe() { + super(CalendarScale.class, "CALSCALE"); + } + + @Override + protected CalendarScale newInstance(String value, ICalVersion version) { + return new CalendarScale(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/CategoriesScribe.java b/app/src/main/java/biweekly/io/scribe/property/CategoriesScribe.java new file mode 100644 index 0000000000..470f01124d --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/CategoriesScribe.java @@ -0,0 +1,45 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.parameter.ICalParameters; +import biweekly.property.Categories; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Categories} properties. + * @author Michael Angstadt + */ +public class CategoriesScribe extends TextListPropertyScribe { + public CategoriesScribe() { + super(Categories.class, "CATEGORIES"); + } + + @Override + public Categories newInstance(ICalDataType dataType, ICalParameters parameters) { + return new Categories(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ClassificationScribe.java b/app/src/main/java/biweekly/io/scribe/property/ClassificationScribe.java new file mode 100644 index 0000000000..f0fb96501e --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ClassificationScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Classification; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Classification} properties. + * @author Michael Angstadt + */ +public class ClassificationScribe extends TextPropertyScribe { + public ClassificationScribe() { + super(Classification.class, "CLASS"); + } + + @Override + protected Classification newInstance(String value, ICalVersion version) { + return new Classification(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/ColorScribe.java b/app/src/main/java/biweekly/io/scribe/property/ColorScribe.java new file mode 100644 index 0000000000..64c966779f --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ColorScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Color; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Color} properties. + * @author Michael Angstadt + */ +public class ColorScribe extends TextPropertyScribe { + public ColorScribe() { + super(Color.class, "COLOR"); + } + + @Override + protected Color newInstance(String value, ICalVersion version) { + return new Color(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/CommentScribe.java b/app/src/main/java/biweekly/io/scribe/property/CommentScribe.java new file mode 100644 index 0000000000..72e3b0df68 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/CommentScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.Comment; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Comment} properties. + * @author Michael Angstadt + */ +public class CommentScribe extends TextPropertyScribe { + public CommentScribe() { + super(Comment.class, "COMMENT"); + } + + @Override + protected Comment newInstance(String value, ICalVersion version) { + return new Comment(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/CompletedScribe.java b/app/src/main/java/biweekly/io/scribe/property/CompletedScribe.java new file mode 100644 index 0000000000..0505d583ca --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/CompletedScribe.java @@ -0,0 +1,46 @@ +package biweekly.io.scribe.property; + +import java.util.Date; + +import biweekly.property.Completed; + + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Completed} properties. + * @author Michael Angstadt + */ +public class CompletedScribe extends DateTimePropertyScribe { + public CompletedScribe() { + super(Completed.class, "COMPLETED"); + } + + @Override + protected Completed newInstance(Date date) { + return new Completed(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ConferenceScribe.java b/app/src/main/java/biweekly/io/scribe/property/ConferenceScribe.java new file mode 100644 index 0000000000..1debb86704 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ConferenceScribe.java @@ -0,0 +1,114 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.Conference; +import biweekly.util.DataUri; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Conference} properties. + * @author Michael Angstadt + */ +public class ConferenceScribe extends ICalPropertyScribe { + public ConferenceScribe() { + super(Conference.class, "CONFERENCE", ICalDataType.URI); + } + + @Override + protected String _writeText(Conference property, WriteContext context) { + return write(property); + } + + @Override + protected Conference _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value); + } + + @Override + protected void _writeXml(Conference property, XCalElement element, WriteContext context) { + element.append(ICalDataType.URI, write(property)); + } + + @Override + protected Conference _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + String uri = element.first(ICalDataType.URI); + if (uri != null) { + return parse(uri); + } + + throw missingXmlElements(ICalDataType.URI); + } + + @Override + protected JCalValue _writeJson(Conference property, WriteContext context) { + return JCalValue.single(write(property)); + } + + @Override + protected Conference _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String uri = value.asSingle(); + return parse(uri); + } + + private static String write(Conference property) { + String uri = property.getUri(); + if (uri != null) { + return uri; + } + + String text = property.getText(); + if (text != null) { + return new DataUri(text).toString(); + } + + return ""; + } + + private static Conference parse(String value) { + try { + DataUri uri = DataUri.parse(value); + String text = uri.getText(); + if (text != null) { + Conference property = new Conference((String) null); + property.setText(text); + return property; + } + } catch (IllegalArgumentException e) { + //not a data URI + } + + return new Conference(value); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ContactScribe.java b/app/src/main/java/biweekly/io/scribe/property/ContactScribe.java new file mode 100644 index 0000000000..e3f15f493b --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ContactScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.Contact; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Contact} properties. + * @author Michael Angstadt + */ +public class ContactScribe extends TextPropertyScribe { + public ContactScribe() { + super(Contact.class, "CONTACT"); + } + + @Override + protected Contact newInstance(String value, ICalVersion version) { + return new Contact(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/CreatedScribe.java b/app/src/main/java/biweekly/io/scribe/property/CreatedScribe.java new file mode 100644 index 0000000000..2a9bdbea9f --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/CreatedScribe.java @@ -0,0 +1,51 @@ +package biweekly.io.scribe.property; + +import java.util.Date; + +import biweekly.ICalVersion; +import biweekly.property.Created; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Created} properties. + * @author Michael Angstadt + */ +public class CreatedScribe extends DateTimePropertyScribe { + public CreatedScribe() { + super(Created.class, "CREATED"); + } + + @Override + public String getPropertyName(ICalVersion version) { + return (version == ICalVersion.V1_0) ? "DCREATED" : super.getPropertyName(version); + } + + @Override + protected Created newInstance(Date date) { + return new Created(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DateDueScribe.java b/app/src/main/java/biweekly/io/scribe/property/DateDueScribe.java new file mode 100644 index 0000000000..dd07259773 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DateDueScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.property.DateDue; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link DateDue} properties. + * @author Michael Angstadt + */ +public class DateDueScribe extends DateOrDateTimePropertyScribe { + public DateDueScribe() { + super(DateDue.class, "DUE"); + } + + @Override + protected DateDue newInstance(ICalDate date) { + return new DateDue(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DateEndScribe.java b/app/src/main/java/biweekly/io/scribe/property/DateEndScribe.java new file mode 100644 index 0000000000..9c450a3fa1 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DateEndScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.property.DateEnd; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link DateEnd} properties. + * @author Michael Angstadt + */ +public class DateEndScribe extends DateOrDateTimePropertyScribe { + public DateEndScribe() { + super(DateEnd.class, "DTEND"); + } + + @Override + protected DateEnd newInstance(ICalDate date) { + return new DateEnd(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DateOrDateTimePropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/DateOrDateTimePropertyScribe.java new file mode 100644 index 0000000000..78fe9cffc3 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DateOrDateTimePropertyScribe.java @@ -0,0 +1,137 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.DateOrDateTimeProperty; +import biweekly.util.ICalDate; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that have either "date" or "date-time" values. + * @param the property class + * @author Michael Angstadt + */ +public abstract class DateOrDateTimePropertyScribe extends ICalPropertyScribe { + public DateOrDateTimePropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName, ICalDataType.DATE_TIME); + } + + @Override + protected ICalParameters _prepareParameters(T property, WriteContext context) { + ICalDate value = property.getValue(); + if (value == null) { + return property.getParameters(); + } + + return handleTzidParameter(property, value.hasTime(), context); + } + + @Override + protected ICalDataType _dataType(T property, ICalVersion version) { + ICalDate value = property.getValue(); + if (value == null) { + return ICalDataType.DATE_TIME; + } + + return value.hasTime() ? ICalDataType.DATE_TIME : ICalDataType.DATE; + } + + @Override + protected String _writeText(T property, WriteContext context) { + ICalDate value = property.getValue(); + return date(value, property, context).extended(false).write(); + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value, parameters, context); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + ICalDataType dataType = dataType(property, null); + ICalDate value = property.getValue(); + + String dateStr = date(value, property, context).extended(true).write(); + element.append(dataType, dateStr); + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + String value = element.first(ICalDataType.DATE_TIME); + if (value == null) { + value = element.first(ICalDataType.DATE); + } + + if (value != null) { + return parse(value, parameters, context); + } + + throw missingXmlElements(ICalDataType.DATE_TIME, ICalDataType.DATE); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + ICalDate value = property.getValue(); + String dateStr = date(value, property, context).extended(true).write(); + return JCalValue.single(dateStr); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = value.asSingle(); + return parse(valueStr, parameters, context); + } + + protected abstract T newInstance(ICalDate date); + + private T parse(String value, ICalParameters parameters, ParseContext context) { + if (value == null) { + return newInstance(null); + } + + ICalDate date; + try { + date = date(value).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(17); + } + + T property = newInstance(date); + context.addDate(date, property, parameters); + return property; + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DateStartScribe.java b/app/src/main/java/biweekly/io/scribe/property/DateStartScribe.java new file mode 100644 index 0000000000..1235dcbdc7 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DateStartScribe.java @@ -0,0 +1,90 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.DateStart; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link DateStart} properties. + * @author Michael Angstadt + */ +public class DateStartScribe extends DateOrDateTimePropertyScribe { + public DateStartScribe() { + super(DateStart.class, "DTSTART"); + } + + @Override + protected ICalParameters _prepareParameters(DateStart property, WriteContext context) { + if (isInObservance(context)) { + return property.getParameters(); + } + return super._prepareParameters(property, context); + } + + @Override + protected String _writeText(DateStart property, WriteContext context) { + if (isInObservance(context)) { + return write(property, false); + } + return super._writeText(property, context); + } + + @Override + protected void _writeXml(DateStart property, XCalElement element, WriteContext context) { + if (isInObservance(context)) { + String dateStr = write(property, true); + ICalDataType dataType = dataType(property, null); + element.append(dataType, dateStr); + return; + } + + super._writeXml(property, element, context); + } + + @Override + protected JCalValue _writeJson(DateStart property, WriteContext context) { + if (isInObservance(context)) { + return JCalValue.single(write(property, true)); + } + return super._writeJson(property, context); + } + + private String write(DateStart property, boolean extended) { + ICalDate value = property.getValue(); + return date(value).observance(true).extended(extended).write(); + } + + @Override + protected DateStart newInstance(ICalDate date) { + return new DateStart(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DateTimePropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/DateTimePropertyScribe.java new file mode 100644 index 0000000000..d309d11e68 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DateTimePropertyScribe.java @@ -0,0 +1,112 @@ +package biweekly.io.scribe.property; + +import java.util.Date; + +import biweekly.ICalDataType; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.DateTimeProperty; +import biweekly.util.ICalDate; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that have "date-time" values. These values will always be + * formatted in the UTC timezone. + * @param the property class + * @author Michael Angstadt + */ +public abstract class DateTimePropertyScribe extends ICalPropertyScribe { + public DateTimePropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName, ICalDataType.DATE_TIME); + } + + @Override + protected String _writeText(T property, WriteContext context) { + Date value = property.getValue(); + return date(value).utc(true).extended(false).write(); + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value, parameters, context); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + ICalDataType dataType = dataType(property, null); + Date value = property.getValue(); + String dateStr = date(value).utc(true).extended(true).write(); + + element.append(dataType, dateStr); + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return parse(value, parameters, context); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + Date value = property.getValue(); + String dateStr = date(value).utc(true).extended(true).write(); + return JCalValue.single(dateStr); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = value.asSingle(); + return parse(valueStr, parameters, context); + } + + private T parse(String value, ICalParameters parameters, ParseContext context) { + ICalDate date; + try { + date = date(value).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(17); + } + + T property = newInstance(date); + context.addDate(date, property, parameters); + return property; + } + + protected abstract T newInstance(Date date); +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DateTimeStampScribe.java b/app/src/main/java/biweekly/io/scribe/property/DateTimeStampScribe.java new file mode 100644 index 0000000000..d07b1ba4f4 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DateTimeStampScribe.java @@ -0,0 +1,63 @@ +package biweekly.io.scribe.property; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.io.SkipMeException; +import biweekly.io.WriteContext; +import biweekly.property.DateTimeStamp; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link DateTimeStamp} properties. + * @author Michael Angstadt + */ +public class DateTimeStampScribe extends DateTimePropertyScribe { + public DateTimeStampScribe() { + super(DateTimeStamp.class, "DTSTAMP"); + } + + @Override + protected DateTimeStamp newInstance(Date date) { + return new DateTimeStamp(date); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } + + @Override + protected String _writeText(DateTimeStamp property, WriteContext context) { + if (context.getVersion() == ICalVersion.V1_0){ + throw new SkipMeException("This property is not used in vCal 1.0."); + } + return super._writeText(property, context); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DaylightScribe.java b/app/src/main/java/biweekly/io/scribe/property/DaylightScribe.java new file mode 100644 index 0000000000..9c3762f28e --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DaylightScribe.java @@ -0,0 +1,129 @@ +package biweekly.io.scribe.property; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.parameter.ICalParameters; +import biweekly.property.Daylight; +import biweekly.util.ICalDate; +import biweekly.util.UtcOffset; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Daylight} properties. + * @author Michael Angstadt + */ +public class DaylightScribe extends ICalPropertyScribe { + public DaylightScribe() { + super(Daylight.class, "DAYLIGHT"); + } + + @Override + protected String _writeText(Daylight property, WriteContext context) { + if (!property.isDaylight()) { + return "FALSE"; + } + + List values = new ArrayList(); + values.add("TRUE"); + + UtcOffset offset = property.getOffset(); + values.add((offset == null) ? "" : offset.toString()); + + ICalDate start = property.getStart(); + values.add((start == null || start.getRawComponents() == null) ? "" : start.getRawComponents().toString(true, false)); + + ICalDate end = property.getEnd(); + values.add((end == null || end.getRawComponents() == null) ? "" : end.getRawComponents().toString(true, false)); + + String standardName = property.getStandardName(); + values.add((standardName == null) ? "" : standardName); + + String daylightName = property.getDaylightName(); + values.add((daylightName == null) ? "" : daylightName); + + return VObjectPropertyValues.writeSemiStructured(values, false, true); + } + + @Override + protected Daylight _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + SemiStructuredValueIterator it = new SemiStructuredValueIterator(value); + + String next = it.next(); + boolean flag = (next == null) ? false : Boolean.parseBoolean(next); + + UtcOffset offset = null; + next = it.next(); + if (next != null) { + try { + offset = UtcOffset.parse(next); + } catch (IllegalArgumentException e) { + throw new CannotParseException(33, next); + } + } + + ICalDate start = null; + next = it.next(); + if (next != null) { + try { + start = date(next).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(34, next); + } + } + + ICalDate end = null; + next = it.next(); + if (next != null) { + try { + end = date(next).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(35, next); + } + } + + String standardName = it.next(); + String daylightName = it.next(); + + return new Daylight(flag, offset, start, end, standardName, daylightName); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V1_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DescriptionScribe.java b/app/src/main/java/biweekly/io/scribe/property/DescriptionScribe.java new file mode 100644 index 0000000000..1f05f5ca6c --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DescriptionScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Description; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Description} properties. + * @author Michael Angstadt + */ +public class DescriptionScribe extends TextPropertyScribe { + public DescriptionScribe() { + super(Description.class, "DESCRIPTION"); + } + + @Override + protected Description newInstance(String value, ICalVersion version) { + return new Description(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/DisplayAlarmScribe.java b/app/src/main/java/biweekly/io/scribe/property/DisplayAlarmScribe.java new file mode 100644 index 0000000000..add2a7fe45 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DisplayAlarmScribe.java @@ -0,0 +1,67 @@ +package biweekly.io.scribe.property; + +import java.util.Collections; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.component.VAlarm; +import biweekly.property.Action; +import biweekly.property.DisplayAlarm; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link DisplayAlarm} properties. + * @author Michael Angstadt + */ +public class DisplayAlarmScribe extends VCalAlarmPropertyScribe { + public DisplayAlarmScribe() { + super(DisplayAlarm.class, "DALARM", ICalDataType.TEXT); + } + + @Override + protected List writeData(DisplayAlarm property) { + String text = property.getText(); + return (text == null) ? Collections.emptyList() : Collections.singletonList(text); + } + + @Override + protected DisplayAlarm create(ICalDataType dataType, SemiStructuredValueIterator it) { + return new DisplayAlarm(it.next()); + } + + @Override + protected void toVAlarm(VAlarm valarm, DisplayAlarm property) { + valarm.setDescription(property.getText()); + } + + @Override + protected Action action() { + return Action.display(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/DurationPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/DurationPropertyScribe.java new file mode 100644 index 0000000000..f4cdf4958e --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/DurationPropertyScribe.java @@ -0,0 +1,116 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.DurationProperty; +import biweekly.util.Duration; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link DurationProperty} properties. + * @author Michael Angstadt + */ +public class DurationPropertyScribe extends ICalPropertyScribe { + public DurationPropertyScribe() { + super(DurationProperty.class, "DURATION", ICalDataType.DURATION); + } + + @Override + protected String _writeText(DurationProperty property, WriteContext context) { + Duration duration = property.getValue(); + if (duration != null) { + return duration.toString(); + } + + return ""; + } + + @Override + protected DurationProperty _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value); + } + + @Override + protected void _writeXml(DurationProperty property, XCalElement element, WriteContext context) { + String durationStr = null; + + Duration duration = property.getValue(); + if (duration != null) { + durationStr = duration.toString(); + } + + element.append(dataType(property, null), durationStr); + } + + @Override + protected DurationProperty _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return parse(value); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(DurationProperty property, WriteContext context) { + Duration value = property.getValue(); + if (value != null) { + return JCalValue.single(value.toString()); + } + + return JCalValue.single(""); + } + + @Override + protected DurationProperty _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = value.asSingle(); + return parse(valueStr); + } + + private DurationProperty parse(String value) { + if (value == null) { + return new DurationProperty((Duration) null); + } + + try { + Duration duration = Duration.parse(value); + return new DurationProperty(duration); + } catch (IllegalArgumentException e) { + throw new CannotParseException(18); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/EmailAlarmScribe.java b/app/src/main/java/biweekly/io/scribe/property/EmailAlarmScribe.java new file mode 100644 index 0000000000..c7c614043c --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/EmailAlarmScribe.java @@ -0,0 +1,92 @@ +package biweekly.io.scribe.property; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.component.VAlarm; +import biweekly.property.Action; +import biweekly.property.Attendee; +import biweekly.property.EmailAlarm; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link EmailAlarm} properties. + * @author Michael Angstadt + */ +public class EmailAlarmScribe extends VCalAlarmPropertyScribe { + public EmailAlarmScribe() { + super(EmailAlarm.class, "MALARM"); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + return ICalDataType.TEXT; + } + + @Override + protected List writeData(EmailAlarm property) { + String email = property.getEmail(); + String note = property.getNote(); + if (email == null && note == null) { + return Collections.emptyList(); + } + + List dataValues = new ArrayList(2); + dataValues.add((email == null) ? "" : email); + dataValues.add((note == null) ? "" : note); + return dataValues; + } + + @Override + protected EmailAlarm create(ICalDataType dataType, SemiStructuredValueIterator it) { + String email = it.next(); + String note = it.next(); + + EmailAlarm property = new EmailAlarm(email); + property.setNote(note); + return property; + } + + @Override + protected void toVAlarm(VAlarm valarm, EmailAlarm property) { + String email = property.getEmail(); + if (email != null) { + valarm.addAttendee(new Attendee(null, email)); + } + valarm.setDescription(property.getNote()); + } + + @Override + protected Action action() { + return Action.email(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ExceptionDatesScribe.java b/app/src/main/java/biweekly/io/scribe/property/ExceptionDatesScribe.java new file mode 100644 index 0000000000..b3acdae457 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ExceptionDatesScribe.java @@ -0,0 +1,191 @@ +package biweekly.io.scribe.property; + +import static biweekly.ICalDataType.DATE; +import static biweekly.ICalDataType.DATE_TIME; + +import java.util.ArrayList; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.ExceptionDates; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link ExceptionDates} properties. + * @author Michael Angstadt + */ +public class ExceptionDatesScribe extends ListPropertyScribe { + public ExceptionDatesScribe() { + super(ExceptionDates.class, "EXDATE"); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + return DATE_TIME; + } + + @Override + protected ICalParameters _prepareParameters(ExceptionDates property, WriteContext context) { + if (isInObservance(context)) { + return property.getParameters(); + } + + boolean hasTime; + if (property.getValues().isEmpty()) { + hasTime = false; + } else { + hasTime = (dataType(property, context.getVersion()) == DATE_TIME); + } + return handleTzidParameter(property, hasTime, context); + } + + @Override + protected ICalDataType _dataType(ExceptionDates property, ICalVersion version) { + List dates = property.getValues(); + if (!dates.isEmpty()) { + return dates.get(0).hasTime() ? DATE_TIME : DATE; + } + + return defaultDataType(version); + } + + @Override + protected ExceptionDates newInstance(ICalDataType dataType, ICalParameters parameters) { + return new ExceptionDates(); + } + + @Override + protected String writeValue(ExceptionDates property, ICalDate value, WriteContext context) { + if (isInObservance(context)) { + return date(value).observance(true).extended(false).write(); + } + + return date(value, property, context).extended(false).write(); + } + + @Override + protected ICalDate readValue(ExceptionDates property, String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + ICalDate date; + try { + boolean hasTime = (dataType == DATE_TIME); + date = date(value).hasTime(hasTime).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(19); + } + context.addDate(date, property, parameters); + + return date; + } + + @Override + protected void _writeXml(ExceptionDates property, XCalElement element, WriteContext context) { + List values = property.getValues(); + if (values.isEmpty()) { + element.append(defaultDataType(context.getVersion()), ""); + return; + } + + if (isInObservance(context)) { + for (ICalDate value : values) { + String valueStr = date(value).observance(true).extended(true).write(); + element.append(DATE_TIME, valueStr); + } + return; + } + + for (ICalDate value : values) { + ICalDataType dataType = value.hasTime() ? DATE_TIME : DATE; + String dateStr = date(value, property, context).extended(true).write(); + element.append(dataType, dateStr); + } + } + + @Override + protected ExceptionDates _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + List dateTimeElements = element.all(DATE_TIME); + List dateElements = element.all(DATE); + if (dateTimeElements.isEmpty() && dateElements.isEmpty()) { + throw missingXmlElements(DATE_TIME, DATE); + } + + ExceptionDates property = new ExceptionDates(); + List values = property.getValues(); + for (String value : dateTimeElements) { + ICalDate datetime = readValue(property, value, DATE_TIME, parameters, context); + values.add(datetime); + } + for (String value : dateElements) { + ICalDate date = readValue(property, value, DATE, parameters, context); + values.add(date); + } + return property; + } + + @Override + protected JCalValue _writeJson(ExceptionDates property, WriteContext context) { + List values = property.getValues(); + if (values.isEmpty()) { + return JCalValue.single(""); + } + + List valuesStr = new ArrayList(); + if (isInObservance(context)) { + for (ICalDate value : values) { + String valueStr = date(value).observance(true).extended(true).write(); + valuesStr.add(valueStr); + } + return JCalValue.multi(valuesStr); + } + + for (ICalDate value : values) { + String dateStr = date(value, property, context).extended(true).write(); + valuesStr.add(dateStr); + } + return JCalValue.multi(valuesStr); + } + + @Override + protected ExceptionDates _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + List valueStrs = value.asMulti(); + + ExceptionDates property = new ExceptionDates(); + List values = property.getValues(); + for (String valueStr : valueStrs) { + ICalDate date = readValue(property, valueStr, dataType, parameters, context); + values.add(date); + } + return property; + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ExceptionRuleScribe.java b/app/src/main/java/biweekly/io/scribe/property/ExceptionRuleScribe.java new file mode 100644 index 0000000000..b816679a49 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ExceptionRuleScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.property.ExceptionRule; +import biweekly.util.Recurrence; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link ExceptionRule} properties. + * @author Michael Angstadt + */ +public class ExceptionRuleScribe extends RecurrencePropertyScribe { + public ExceptionRuleScribe() { + super(ExceptionRule.class, "EXRULE"); + } + + @Override + protected ExceptionRule newInstance(Recurrence recur) { + return new ExceptionRule(recur); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/FreeBusyScribe.java b/app/src/main/java/biweekly/io/scribe/property/FreeBusyScribe.java new file mode 100644 index 0000000000..4977e4e8ea --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/FreeBusyScribe.java @@ -0,0 +1,253 @@ +package biweekly.io.scribe.property; + +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.FreeBusy; +import biweekly.util.Duration; +import biweekly.util.ICalDate; +import biweekly.util.Period; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link FreeBusy} properties. + * @author Michael Angstadt + */ +public class FreeBusyScribe extends ICalPropertyScribe { + public FreeBusyScribe() { + super(FreeBusy.class, "FREEBUSY"); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + return ICalDataType.PERIOD; + } + + @Override + protected String _writeText(final FreeBusy property, final WriteContext context) { + List values = property.getValues(); + List strValues = new ArrayList(values.size()); + for (Period period : values) { + StringBuilder sb = new StringBuilder(); + + Date start = period.getStartDate(); + if (start != null) { + String dateStr = date(start, property, context).extended(false).write(); + sb.append(dateStr); + } + + sb.append('/'); + + Date end = period.getEndDate(); + if (end != null) { + String dateStr = date(end, property, context).extended(false).write(); + sb.append(dateStr); + } else if (period.getDuration() != null) { + sb.append(period.getDuration()); + } + + strValues.add(sb.toString()); + } + + return VObjectPropertyValues.writeList(strValues); + } + + @Override + protected FreeBusy _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(VObjectPropertyValues.parseList(value), parameters, context); + } + + @Override + protected void _writeXml(FreeBusy property, XCalElement element, WriteContext context) { + for (Period period : property.getValues()) { + XCalElement periodElement = element.append(ICalDataType.PERIOD); + + Date start = period.getStartDate(); + if (start != null) { + String dateStr = date(start, property, context).extended(true).write(); + periodElement.append("start", dateStr); + } + + Date end = period.getEndDate(); + if (end != null) { + String dateStr = date(end, property, context).extended(true).write(); + periodElement.append("end", dateStr); + } + + Duration duration = period.getDuration(); + if (duration != null) { + periodElement.append("duration", duration.toString()); + } + } + } + + @Override + protected FreeBusy _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + List periodElements = element.children(ICalDataType.PERIOD); + if (periodElements.isEmpty()) { + throw missingXmlElements(ICalDataType.PERIOD); + } + + FreeBusy property = new FreeBusy(); + for (XCalElement periodElement : periodElements) { + String startStr = periodElement.first("start"); + if (startStr == null) { + throw new CannotParseException(9); + } + + ICalDate start; + try { + start = date(startStr).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(10, startStr); + } + + String endStr = periodElement.first("end"); + if (endStr != null) { + try { + ICalDate end = date(endStr).parse(); + property.getValues().add(new Period(start, end)); + context.addDate(start, property, parameters); + context.addDate(end, property, parameters); + } catch (IllegalArgumentException e) { + throw new CannotParseException(11, endStr); + } + continue; + } + + String durationStr = periodElement.first("duration"); + if (durationStr != null) { + try { + Duration duration = Duration.parse(durationStr); + property.getValues().add(new Period(start, duration)); + context.addDate(start, property, parameters); + } catch (IllegalArgumentException e) { + throw new CannotParseException(12, durationStr); + } + continue; + } + + throw new CannotParseException(13); + } + + return property; + } + + @Override + protected JCalValue _writeJson(FreeBusy property, WriteContext context) { + List values = property.getValues(); + if (values.isEmpty()) { + return JCalValue.single(""); + } + + List valuesStr = new ArrayList(); + for (Period period : values) { + StringBuilder sb = new StringBuilder(); + Date start = period.getStartDate(); + if (start != null) { + String dateStr = date(start, property, context).extended(true).write(); + sb.append(dateStr); + } + + sb.append('/'); + + Date end = period.getEndDate(); + if (end != null) { + String dateStr = date(end, property, context).extended(true).write(); + sb.append(dateStr); + } else if (period.getDuration() != null) { + sb.append(period.getDuration()); + } + + valuesStr.add(sb.toString()); + } + + return JCalValue.multi(valuesStr); + } + + @Override + protected FreeBusy _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(value.asMulti(), parameters, context); + } + + private FreeBusy parse(List periods, ICalParameters parameters, ParseContext context) { + FreeBusy property = new FreeBusy(); + + for (String period : periods) { + int slash = period.indexOf('/'); + if (slash < 0) { + throw new CannotParseException(13); + } + + String startStr = period.substring(0, slash); + ICalDate start; + try { + start = date(startStr).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(10, startStr); + } + + String endStr = period.substring(slash + 1); + ICalDate end; + try { + end = date(endStr).parse(); + property.getValues().add(new Period(start, end)); + context.addDate(start, property, parameters); + context.addDate(end, property, parameters); + } catch (IllegalArgumentException e) { + //must be a duration + try { + Duration duration = Duration.parse(endStr); + property.getValues().add(new Period(start, duration)); + context.addDate(start, property, parameters); + } catch (IllegalArgumentException e2) { + throw new CannotParseException(14, endStr); + } + } + } + + return property; + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/GeoScribe.java b/app/src/main/java/biweekly/io/scribe/property/GeoScribe.java new file mode 100644 index 0000000000..ed16ed0904 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/GeoScribe.java @@ -0,0 +1,170 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.Geo; +import biweekly.util.ICalFloatFormatter; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.StructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Geo} properties. + * @author Michael Angstadt + */ +public class GeoScribe extends ICalPropertyScribe { + public GeoScribe() { + super(Geo.class, "GEO"); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + return ICalDataType.FLOAT; + } + + @Override + protected String _writeText(Geo property, WriteContext context) { + ICalFloatFormatter formatter = new ICalFloatFormatter(); + + Double latitude = property.getLatitude(); + if (latitude == null) { + latitude = 0.0; + } + String latitudeStr = formatter.format(latitude); + + Double longitude = property.getLongitude(); + if (longitude == null) { + longitude = 0.0; + } + String longitudeStr = formatter.format(longitude); + + char delimiter = getDelimiter(context.getVersion()); + return latitudeStr + delimiter + longitudeStr; + } + + @Override + protected Geo _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + char delimiter = getDelimiter(context.getVersion()); + int pos = value.indexOf(delimiter); + if (pos < 0) { + throw new CannotParseException(20); + } + + String latitudeStr = value.substring(0, pos); + String longitudeStr = value.substring(pos + 1); + return parse(latitudeStr, longitudeStr); + } + + @Override + protected void _writeXml(Geo property, XCalElement element, WriteContext context) { + ICalFloatFormatter formatter = new ICalFloatFormatter(); + + Double latitude = property.getLatitude(); + if (latitude == null) { + latitude = 0.0; + } + element.append("latitude", formatter.format(latitude)); + + Double longitude = property.getLongitude(); + if (longitude == null) { + longitude = 0.0; + } + element.append("longitude", formatter.format(longitude)); + } + + @Override + protected Geo _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + String latitudeStr = element.first("latitude"); + String longitudeStr = element.first("longitude"); + if (latitudeStr == null && longitudeStr == null) { + throw missingXmlElements("latitude", "longitude"); + } + if (latitudeStr == null) { + throw missingXmlElements("latitude"); + } + if (longitudeStr == null) { + throw missingXmlElements("longitude"); + } + + return parse(latitudeStr, longitudeStr); + } + + @Override + protected JCalValue _writeJson(Geo property, WriteContext context) { + Double latitude = property.getLatitude(); + if (latitude == null) { + latitude = 0.0; + } + + Double longitude = property.getLongitude(); + if (longitude == null) { + longitude = 0.0; + } + + return JCalValue.structured(latitude, longitude); + } + + @Override + protected Geo _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + StructuredValueIterator it = new StructuredValueIterator(value.asStructured()); + String latitudeStr = it.nextValue(); + String longitudeStr = it.nextValue(); + return parse(latitudeStr, longitudeStr); + } + + private char getDelimiter(ICalVersion version) { + return (version == ICalVersion.V1_0) ? ',' : ';'; + } + + private Geo parse(String latitudeStr, String longitudeStr) { + Double latitude = null; + if (latitudeStr != null) { + try { + latitude = Double.valueOf(latitudeStr); + } catch (NumberFormatException e) { + throw new CannotParseException(21, latitudeStr); + } + } + + Double longitude = null; + if (longitudeStr != null) { + try { + longitude = Double.valueOf(longitudeStr); + } catch (NumberFormatException e) { + throw new CannotParseException(22, longitudeStr); + } + } + + return new Geo(latitude, longitude); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ICalPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/ICalPropertyScribe.java new file mode 100644 index 0000000000..f07c570acd --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ICalPropertyScribe.java @@ -0,0 +1,884 @@ +package biweekly.io.scribe.property; + +import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.TimeZone; + +import javax.xml.namespace.QName; + +import org.w3c.dom.Element; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.ValidationWarning; +import biweekly.component.Observance; +import biweekly.component.VTimezone; +import biweekly.io.CannotParseException; +import biweekly.io.DataModelConversionException; +import biweekly.io.ParseContext; +import biweekly.io.SkipMeException; +import biweekly.io.TimezoneAssignment; +import biweekly.io.TimezoneInfo; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.json.JsonValue; +import biweekly.io.xml.XCalElement; +import biweekly.io.xml.XCalElement.XCalValue; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.property.ValuedProperty; +import biweekly.util.DateTimeComponents; +import biweekly.util.ICalDate; +import biweekly.util.ICalDateFormat; +import biweekly.util.ListMultimap; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Base class for iCalendar property scribes. + * @param the property class + * @author Michael Angstadt + */ +public abstract class ICalPropertyScribe { + private static final Set allVersions = Collections.unmodifiableSet(EnumSet.allOf(ICalVersion.class)); + + protected final Class clazz; + protected final String propertyName; + private final ICalDataType defaultDataType; + protected final QName qname; + + /** + * Creates a new scribe. + * @param clazz the property class + * @param propertyName the property name (e.g. "VERSION") + */ + public ICalPropertyScribe(Class clazz, String propertyName) { + this(clazz, propertyName, null); + } + + /** + * Creates a new scribe. + * @param clazz the property class + * @param propertyName the property name (e.g. "VERSION") + * @param defaultDataType the property's default data type (e.g. "text") or + * null if unknown + */ + public ICalPropertyScribe(Class clazz, String propertyName, ICalDataType defaultDataType) { + this(clazz, propertyName, defaultDataType, new QName(XCAL_NS, propertyName.toLowerCase())); + } + + /** + * Creates a new scribe. + * @param clazz the property class + * @param propertyName the property name (e.g. "VERSION") + * @param defaultDataType the property's default data type (e.g. "text") or + * null if unknown + * @param qname the XML element name and namespace to use for xCal documents + * (by default, the XML element name is set to the lower-cased property + * name, and the element namespace is set to the xCal namespace) + */ + public ICalPropertyScribe(Class clazz, String propertyName, ICalDataType defaultDataType, QName qname) { + this.clazz = clazz; + this.propertyName = propertyName; + this.defaultDataType = defaultDataType; + this.qname = qname; + } + + /** + * Gets the iCalendar versions that support this property. This method + * returns all iCalendar versions unless overridden by the child scribe. + * @return the iCalendar versions + */ + public Set getSupportedVersions() { + return allVersions; + } + + /** + * Gets the property class. + * @return the property class + */ + public Class getPropertyClass() { + return clazz; + } + + /** + * Gets the property name. Child classes should override this method if the + * property's name differs between versions. + * @param version the iCalendar version (a few properties have different + * names under different versions) + * @return the property name (e.g. "DTSTART") + */ + public String getPropertyName(ICalVersion version) { + return propertyName; + } + + /** + * Gets this property's local name and namespace for xCal documents. + * @return the XML local name and namespace + */ + public QName getQName() { + return qname; + } + + /** + * Sanitizes a property's parameters before the property is written. + * @param property the property to write + * @param context the context + * @return the sanitized parameters + */ + public final ICalParameters prepareParameters(T property, WriteContext context) { + return _prepareParameters(property, context); + } + + /** + * Determines the default data type of this property. + * @param version the version of the iCalendar object being generated + * @return the data type or null if unknown + */ + public final ICalDataType defaultDataType(ICalVersion version) { + return _defaultDataType(version); + } + + /** + * Determines the data type of a property instance. + * @param property the property + * @param version the version of the iCalendar object being generated + * @return the data type or null if unknown + */ + public final ICalDataType dataType(T property, ICalVersion version) { + return _dataType(property, version); + } + + /** + * Marshals a property's value to a string. + * @param property the property + * @param context the context + * @return the marshalled property value + * @throws SkipMeException if the property should not be written to the data + * stream + * @throws DataModelConversionException if the property needs to be + * converted to something different in order to adhere to the data model of + * the iCalendar version being written (only applicable when writing 1.0 + * vCals) + */ + public final String writeText(T property, WriteContext context) { + return _writeText(property, context); + } + + /** + * Marshals a property's value to an XML element (xCal). + * @param property the property + * @param element the property's XML element + * @param context the context + * @throws SkipMeException if the property should not be written to the data + * stream + */ + public final void writeXml(T property, Element element, WriteContext context) { + XCalElement xcalElement = new XCalElement(element); + _writeXml(property, xcalElement, context); + } + + /** + * Marshals a property's value to a JSON data stream (jCal). + * @param property the property + * @param context the context + * @return the marshalled value + * @throws SkipMeException if the property should not be written to the data + * stream + */ + public final JCalValue writeJson(T property, WriteContext context) { + return _writeJson(property, context); + } + + /** + * Unmarshals a property from a plain-text iCalendar data stream. + * @param value the value as read off the wire + * @param dataType the data type of the property value. The property's VALUE + * parameter is used to determine the data type. If the property has no + * VALUE parameter, then this parameter will be set to the property's + * default datatype. Note that the VALUE parameter is removed from the + * property's parameter list after it has been read. + * @param parameters the parsed parameters + * @param context the parse context + * @return the unmarshalled property + * @throws CannotParseException if the scribe could not parse the property's + * value + * @throws SkipMeException if the property should not be added to the final + * {@link ICalendar} object + * @throws DataModelConversionException if the property should be converted + * to something different in order to adhere to the 2.0 data model (only + * thrown when parsing 1.0 vCals) + */ + public final T parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + T property = _parseText(value, dataType, parameters, context); + property.setParameters(parameters); + return property; + } + + /** + * Unmarshals a property's value from an XML document (xCal). + * @param element the property's XML element + * @param parameters the property's parameters + * @param context the context + * @return the unmarshalled property + * @throws CannotParseException if the scribe could not parse the property's + * value + * @throws SkipMeException if the property should not be added to the final + * {@link ICalendar} object + */ + public final T parseXml(Element element, ICalParameters parameters, ParseContext context) { + T property = _parseXml(new XCalElement(element), parameters, context); + property.setParameters(parameters); + return property; + } + + /** + * Unmarshals a property's value from a JSON data stream (jCal). + * @param value the property's JSON value + * @param dataType the data type + * @param parameters the parsed parameters + * @param context the context + * @return the unmarshalled property + * @throws CannotParseException if the scribe could not parse the property's + * value + * @throws SkipMeException if the property should not be added to the final + * {@link ICalendar} object + */ + public final T parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + T property = _parseJson(value, dataType, parameters, context); + property.setParameters(parameters); + return property; + } + + /** + *

+ * Sanitizes a property's parameters before the property is written. + *

+ *

+ * This method should be overridden by child classes that wish to tweak the + * property's parameters before the property is written. The default + * implementation of this method returns the property's parameters + * unmodified. + *

+ * @param property the property to write + * @param context the context + * @return the sanitized parameters (this should be a *copy* of the + * property's parameters if modifications were made) + */ + protected ICalParameters _prepareParameters(T property, WriteContext context) { + return property.getParameters(); + } + + /** + *

+ * Determines the default data type of this property. + *

+ *

+ * This method should be overridden by child classes if a property's default + * data type changes depending the iCalendar version. The default + * implementation of this method returns the data type that was passed into + * the {@link #ICalPropertyScribe(Class, String, ICalDataType)} constructor. + * Null is returned if this constructor was not invoked. + *

+ * @param version the version of the iCalendar object being generated + * @return the data type or null if unknown + */ + protected ICalDataType _defaultDataType(ICalVersion version) { + return defaultDataType; + } + + /** + *

+ * Determines the data type of a property instance. + *

+ *

+ * This method should be overridden by child classes if a property's data + * type changes depending on its value. The default implementation of this + * method returns the property's default data type. + *

+ * @param property the property + * @param version the version of the iCalendar object being generated + * @return the data type or null if unknown + */ + protected ICalDataType _dataType(T property, ICalVersion version) { + return defaultDataType(version); + } + + /** + * Marshals a property's value to a string. + * @param property the property + * @param context the write context + * @return the marshalled value + * @throws SkipMeException if the property should not be written to the data + * stream + * @throws DataModelConversionException if the property needs to be + * converted to something different in order to adhere to the data model of + * the iCalendar version being written (only applicable when writing 1.0 + * vCals) + */ + protected abstract String _writeText(T property, WriteContext context); + + /** + *

+ * Marshals a property's value to an XML element (xCal). + *

+ *

+ * This method should be overridden by child classes that wish to support + * xCal. The default implementation of this method will append one child + * element to the property's XML element. The child element's name will be + * that of the property's data type (retrieved using the {@link #dataType} + * method), and the child element's text content will be set to the + * property's marshalled plain-text value (retrieved using the + * {@link #writeText} method). + *

+ * @param property the property + * @param element the property's XML element + * @param context the context + * @throws SkipMeException if the property should not be written to the data + * stream + */ + protected void _writeXml(T property, XCalElement element, WriteContext context) { + String value = writeText(property, context); + ICalDataType dataType = dataType(property, ICalVersion.V2_0); + element.append(dataType, value); + } + + /** + *

+ * Marshals a property's value to a JSON data stream (jCal). + *

+ *

+ * This method should be overridden by child classes that wish to support + * jCal. The default implementation of this method will create a jCard + * property that has a single JSON string value (generated by the + * {@link #writeText} method). + *

+ * @param property the property + * @param context the context + * @return the marshalled value + * @throws SkipMeException if the property should not be written to the data + * stream + */ + protected JCalValue _writeJson(T property, WriteContext context) { + String value = writeText(property, context); + return JCalValue.single(value); + } + + /** + * Unmarshals a property from a plain-text iCalendar data stream. + * @param value the value as read off the wire + * @param dataType the data type of the property value. The property's VALUE + * parameter is used to determine the data type. If the property has no + * VALUE parameter, then this parameter will be set to the property's + * default datatype. Note that the VALUE parameter is removed from the + * property's parameter list after it has been read. + * @param parameters the parsed parameters. These parameters will be + * assigned to the property object once this method returns. Therefore, do + * not assign any parameters to the property object itself whilst inside of + * this method, or else they will be overwritten. + * @param context the parse context + * @return the unmarshalled property object + * @throws CannotParseException if the scribe could not parse the property's + * value + * @throws SkipMeException if the property should not be added to the final + * {@link ICalendar} object + * @throws DataModelConversionException if the property should be converted + * to something different in order to adhere to the 2.0 data model (only + * thrown when parsing 1.0 vCals) + */ + protected abstract T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context); + + /** + *

+ * Unmarshals a property from an XML document (xCal). + *

+ *

+ * This method should be overridden by child classes that wish to support + * xCal. The default implementation of this method will find the first child + * element with the xCal namespace. The element's name will be used as the + * property's data type and its text content will be passed into the + * {@link #_parseText} method. If no such child element is found, then the + * parent element's text content will be passed into {@link #_parseText} and + * the data type will be null. + *

+ * @param element the property's XML element + * @param parameters the parsed parameters. These parameters will be + * assigned to the property object once this method returns. Therefore, do + * not assign any parameters to the property object itself whilst inside of + * this method, or else they will be overwritten. + * @param context the context + * @return the unmarshalled property object + * @throws CannotParseException if the scribe could not parse the property's + * value + * @throws SkipMeException if the property should not be added to the final + * {@link ICalendar} object + */ + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + XCalValue firstValue = element.firstValue(); + ICalDataType dataType = firstValue.getDataType(); + String value = VObjectPropertyValues.escape(firstValue.getValue()); + return _parseText(value, dataType, parameters, context); + } + + /** + *

+ * Unmarshals a property from a JSON data stream (jCal). + *

+ *

+ * This method should be overridden by child classes that wish to support + * jCal. The default implementation of this method will convert the jCal + * property value to a string and pass it into the {@link #_parseText} + * method. + *

+ * + *
+ * + *

+ * The following paragraphs describe the way in which this method's default + * implementation converts a jCal value to a string: + *

+ *

+ * If the jCal value consists of a single, non-array, non-object value, then + * the value is converted to a string. Special characters (backslashes, + * commas, and semicolons) are escaped in order to simulate what the value + * might look like in a plain-text iCalendar object.
+ * ["x-foo", {}, "text", "the;value"] --> "the\;value"
+ * ["x-foo", {}, "text", 2] --> "2" + *

+ *

+ * If the jCal value consists of multiple, non-array, non-object values, + * then all the values are appended together in a single string, separated + * by commas. Special characters (backslashes, commas, and semicolons) are + * escaped for each value in order to prevent commas from being treated as + * delimiters, and to simulate what the value might look like in a + * plain-text iCalendar object.
+ * ["x-foo", {}, "text", "one", "two,three"] --> + * "one,two\,three" + *

+ *

+ * If the jCal value is a single array, then this array is treated as a + * "structured value", and converted its plain-text representation. Special + * characters (backslashes, commas, and semicolons) are escaped for each + * value in order to prevent commas and semicolons from being treated as + * delimiters.
+ * ["x-foo", {}, "text", ["one", ["two", "three"], "four;five"]] + * --> "one;two,three;four\;five" + *

+ *

+ * If the jCal value starts with a JSON object, then the object is converted + * to a format identical to the one used in the RRULE and EXRULE properties. + * Special characters (backslashes, commas, semicolons, and equal signs) are + * escaped for each value in order to preserve the syntax of the string + * value.
+ * ["x-foo", {}, "text", {"one": 1, "two": [2, 2.5]}] --> "ONE=1;TWO=2,2.5" + *

+ *

+ * For all other cases, behavior is undefined. + *

+ * @param value the property's JSON value + * @param dataType the data type + * @param parameters the parsed parameters. These parameters will be + * assigned to the property object once this method returns. Therefore, do + * not assign any parameters to the property object itself whilst inside of + * this method, or else they will be overwritten. + * @param context the context + * @return the unmarshalled property object + * @throws CannotParseException if the scribe could not parse the property's + * value + * @throws SkipMeException if the property should not be added to the final + * {@link ICalendar} object + */ + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = jcalValueToString(value); + return _parseText(valueStr, dataType, parameters, context); + } + + /** + * Converts a jCal value to its plain-text format representation. + * @param value the jCal value + * @return the plain-text format representation (for example, "1,2,3" for a + * list of values) + */ + private static String jcalValueToString(JCalValue value) { + List values = value.getValues(); + if (values.size() > 1) { + List multi = value.asMulti(); + if (!multi.isEmpty()) { + return VObjectPropertyValues.writeList(multi); + } + } + + if (!values.isEmpty() && values.get(0).getArray() != null) { + List> structured = value.asStructured(); + if (!structured.isEmpty()) { + return VObjectPropertyValues.writeStructured(structured, true); + } + } + + if (values.get(0).getObject() != null) { + ListMultimap object = value.asObject(); + if (!object.isEmpty()) { + return VObjectPropertyValues.writeMultimap(object.getMap()); + } + } + + return VObjectPropertyValues.escape(value.asSingle()); + } + + /** + * Determines if the property is within an observance component. + * @param context the write context + * @return true if the property is within an observance, false if not + */ + protected static boolean isInObservance(WriteContext context) { + return context.getParent() instanceof Observance; + } + + /** + * Parses a date string. + * @param value the date string + * @return the factory object + */ + protected static DateParser date(String value) { + return new DateParser(value); + } + + /** + * Factory class for parsing dates. + */ + protected static class DateParser { + private String value; + private Boolean hasTime; + + /** + * Creates a new date writer object. + * @param value the date string to parse + */ + public DateParser(String value) { + this.value = value; + } + + /** + * Forces the value to be parsed as a date-time or date value. + * @param hasTime true to parsed as a date-time value, false to parse as + * a date value, null to parse as whatever value it is (defaults to + * null) + * @return this + */ + public DateParser hasTime(Boolean hasTime) { + this.hasTime = hasTime; + return this; + } + + /** + * Parses the date string. + * @return the parsed date + * @throws IllegalArgumentException if the date string is invalid + */ + public ICalDate parse() { + DateTimeComponents components = DateTimeComponents.parse(value, hasTime); + Date date = components.toDate(); + boolean hasTime = components.hasTime(); + + return new ICalDate(date, components, hasTime); + } + } + + /** + * Formats a date as a string. + * @param date the date + * @return the factory object + */ + protected static DateWriter date(Date date) { + return date((date == null) ? null : new ICalDate(date)); + } + + /** + * Formats a date as a string. + * @param date the date + * @return the factory object + */ + protected static DateWriter date(ICalDate date) { + return new DateWriter(date); + } + + protected static DateWriter date(Date date, ICalProperty property, WriteContext context) { + return date((date == null) ? null : new ICalDate(date), property, context); + } + + protected static DateWriter date(ICalDate date, ICalProperty property, WriteContext context) { + boolean floating; + TimeZone tz; + TimezoneAssignment globalTz = context.getGlobalTimezone(); + if (globalTz == null) { + TimezoneInfo tzinfo = context.getTimezoneInfo(); + floating = tzinfo.isFloating(property); + TimezoneAssignment assignment = tzinfo.getTimezoneToWriteIn(property); + tz = (assignment == null) ? null : assignment.getTimeZone(); + } else { + floating = false; + tz = globalTz.getTimeZone(); + } + + context.addDate(date, floating, tz); + return date(date).tz(floating, tz); + } + + /** + * Factory class for writing dates. + */ + protected static class DateWriter { + private ICalDate date; + private TimeZone timezone; + private boolean observance = false; + private boolean extended = false; + private boolean utc = false; + + /** + * Creates a new date writer object. + * @param date the date to format + */ + public DateWriter(ICalDate date) { + this.date = date; + } + + /** + * Sets whether the property is within an observance or not. + * @param observance true if it's in an observance, false if not + * (defaults to false) + * @return this + */ + public DateWriter observance(boolean observance) { + this.observance = observance; + return this; + } + + /** + * Sets whether to write the value in UTC or not. + * @param utc true to write in UTC, false not to + * @return this + */ + public DateWriter utc(boolean utc) { + this.utc = utc; + return this; + } + + /** + * Sets the timezone. + * @param floating true to use floating time, false not to + * @param timezone the timezone + * @return this + */ + public DateWriter tz(boolean floating, TimeZone timezone) { + if (floating) { + timezone = TimeZone.getDefault(); + } + this.timezone = timezone; + return this; + } + + /** + * Sets whether to use extended format or basic. + * @param extended true to use extended format, false to use basic + * (defaults to "false") + * @return this + */ + public DateWriter extended(boolean extended) { + this.extended = extended; + return this; + } + + /** + * Creates the date string. + * @return the date string + */ + public String write() { + if (date == null) { + return ""; + } + + if (observance) { + DateTimeComponents components = date.getRawComponents(); + if (components == null) { + ICalDateFormat format = extended ? ICalDateFormat.DATE_TIME_EXTENDED_WITHOUT_TZ : ICalDateFormat.DATE_TIME_BASIC_WITHOUT_TZ; + return format.format(date); + } + + return components.toString(true, extended); + } + + if (utc) { + ICalDateFormat format = extended ? ICalDateFormat.UTC_TIME_EXTENDED : ICalDateFormat.UTC_TIME_BASIC; + return format.format(date); + } + + ICalDateFormat format; + TimeZone timezone = this.timezone; + if (date.hasTime()) { + if (timezone == null) { + format = extended ? ICalDateFormat.UTC_TIME_EXTENDED : ICalDateFormat.UTC_TIME_BASIC; + } else { + format = extended ? ICalDateFormat.DATE_TIME_EXTENDED_WITHOUT_TZ : ICalDateFormat.DATE_TIME_BASIC_WITHOUT_TZ; + } + } else { + format = extended ? ICalDateFormat.DATE_EXTENDED : ICalDateFormat.DATE_BASIC; + timezone = null; + } + + return format.format(date, timezone); + } + } + + /** + * Adds a TZID parameter to a property's parameter list if necessary. + * @param property the property + * @param hasTime true if the property value has a time component, false if + * not + * @param context the write context + * @return the property's new set of parameters + */ + protected static ICalParameters handleTzidParameter(ICalProperty property, boolean hasTime, WriteContext context) { + ICalParameters parameters = property.getParameters(); + + //date values don't have timezones + if (!hasTime) { + return parameters; + } + + //vCal doesn't use the TZID parameter + if (context.getVersion() == ICalVersion.V1_0) { + return parameters; + } + + //floating values don't have timezones + TimezoneInfo tzinfo = context.getTimezoneInfo(); + boolean floating = tzinfo.isFloating(property); + if (floating) { + return parameters; + } + + TimezoneAssignment tz; + TimezoneAssignment globalTz = context.getGlobalTimezone(); + if (globalTz == null) { + tz = tzinfo.getTimezoneToWriteIn(property); + if (tz == null) { + //write in UTC + return parameters; + } + } else { + tz = globalTz; + } + + String tzid = null; + VTimezone component = tz.getComponent(); + String globalId = tz.getGlobalId(); + if (component != null) { + tzid = ValuedProperty.getValue(component.getTimezoneId()); + } else if (globalId != null) { + tzid = '/' + globalId; + } + + if (tzid == null) { + //should never happen + tzid = tz.getTimeZone().getID(); + } + + parameters = new ICalParameters(parameters); + parameters.setTimezoneId(tzid); + return parameters; + } + + /** + * Creates a {@link CannotParseException}, indicating that the XML elements + * that the parser expected to find are missing from the property's XML + * element. + * @param dataTypes the expected data types (null for "unknown") + * @return the exception + */ + protected static CannotParseException missingXmlElements(ICalDataType... dataTypes) { + String[] elements = new String[dataTypes.length]; + for (int i = 0; i < dataTypes.length; i++) { + ICalDataType dataType = dataTypes[i]; + elements[i] = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); + } + return missingXmlElements(elements); + } + + /** + * Creates a {@link CannotParseException}, indicating that the XML elements + * that the parser expected to find are missing from property's XML element. + * @param elements the names of the expected XML elements + * @return the exception + */ + protected static CannotParseException missingXmlElements(String... elements) { + return new CannotParseException(23, Arrays.toString(elements)); + } + + /** + * Represents the result of an unmarshal operation. + * @author Michael Angstadt + * @param the unmarshalled property class + */ + public static class Result { + private final T property; + private final List warnings; + + /** + * Creates a new result. + * @param property the property object + * @param warnings the warnings + */ + public Result(T property, List warnings) { + this.property = property; + this.warnings = warnings; + } + + /** + * Gets the warnings. + * @return the warnings + */ + public List getWarnings() { + return warnings; + } + + /** + * Gets the property object. + * @return the property object + */ + public T getProperty() { + return property; + } + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ImageScribe.java b/app/src/main/java/biweekly/io/scribe/property/ImageScribe.java new file mode 100644 index 0000000000..66bb158e62 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ImageScribe.java @@ -0,0 +1,57 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.property.Image; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Image} properties. + * @author Michael Angstadt + */ +public class ImageScribe extends BinaryPropertyScribe { + public ImageScribe() { + super(Image.class, "IMAGE"); + } + + @Override + protected Image newInstance(byte[] data) { + /* + * Note: "formatType" will be set when the parameters are assigned to + * the property object. + */ + return new Image(null, data); + } + + @Override + protected Image newInstance(String value, ICalDataType dataType) { + /* + * Note: "formatType" will be set when the parameters are assigned to + * the property object. + */ + return new Image(null, value); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/IntegerPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/IntegerPropertyScribe.java new file mode 100644 index 0000000000..591db1da6a --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/IntegerPropertyScribe.java @@ -0,0 +1,112 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.IntegerProperty; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that have integer values. + * @param the property class + * @author Michael Angstadt + */ +public abstract class IntegerPropertyScribe extends ICalPropertyScribe { + public IntegerPropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName, ICalDataType.INTEGER); + } + + @Override + protected String _writeText(T property, WriteContext context) { + Integer value = property.getValue(); + if (value != null) { + return value.toString(); + } + + return ""; + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + String valueStr = null; + + Integer value = property.getValue(); + if (value != null) { + valueStr = value.toString(); + } + + element.append(dataType(property, null), valueStr); + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return parse(value); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + return JCalValue.single(property.getValue()); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(value.asSingle()); + } + + private T parse(String value) { + if (value == null || value.isEmpty()) { + return newInstance(null); + } + + try { + Integer intValue = Integer.valueOf(value); + return newInstance(intValue); + } catch (NumberFormatException e) { + throw new CannotParseException(24); + } + } + + protected abstract T newInstance(Integer value); +} diff --git a/app/src/main/java/biweekly/io/scribe/property/LastModifiedScribe.java b/app/src/main/java/biweekly/io/scribe/property/LastModifiedScribe.java new file mode 100644 index 0000000000..9ea5751c38 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/LastModifiedScribe.java @@ -0,0 +1,46 @@ +package biweekly.io.scribe.property; + +import java.util.Date; + +import biweekly.property.LastModified; + + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link LastModified} properties. + * @author Michael Angstadt + */ +public class LastModifiedScribe extends DateTimePropertyScribe { + public LastModifiedScribe() { + super(LastModified.class, "LAST-MODIFIED"); + } + + @Override + protected LastModified newInstance(Date date) { + return new LastModified(date); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ListPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/ListPropertyScribe.java new file mode 100644 index 0000000000..b454a94bef --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ListPropertyScribe.java @@ -0,0 +1,139 @@ +package biweekly.io.scribe.property; + +import java.util.ArrayList; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.ListProperty; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that contain a list of values. + * @param the property class + * @param the value class + * @author Michael Angstadt + */ +public abstract class ListPropertyScribe, V> extends ICalPropertyScribe { + public ListPropertyScribe(Class clazz, String propertyName) { + this(clazz, propertyName, ICalDataType.TEXT); + } + + public ListPropertyScribe(Class clazz, String propertyName, ICalDataType dataType) { + super(clazz, propertyName, dataType); + } + + @Override + protected String _writeText(final T property, WriteContext context) { + List values = property.getValues(); + List valuesStr = new ArrayList(values.size()); + for (V value : values) { + String valueStr = writeValue(property, value, context); + valuesStr.add(valueStr); + } + + switch (context.getVersion()) { + case V1_0: + return VObjectPropertyValues.writeSemiStructured(valuesStr, false, true); + default: + return VObjectPropertyValues.writeList(valuesStr); + } + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + List values; + switch (context.getVersion()) { + case V1_0: + values = VObjectPropertyValues.parseSemiStructured(value); + break; + default: + values = VObjectPropertyValues.parseList(value); + break; + } + + return parse(values, dataType, parameters, context); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + for (V value : property.getValues()) { + String valueStr = writeValue(property, value, null); + element.append(dataType(property, null), valueStr); + } + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + List values = element.all(dataType); + if (!values.isEmpty()) { + return parse(values, dataType, parameters, context); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + List values = property.getValues(); + if (!values.isEmpty()) { + return JCalValue.multi(property.getValues()); + } + + return JCalValue.single(""); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(value.asMulti(), dataType, parameters, context); + } + + private T parse(List valueStrs, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + T property = newInstance(dataType, parameters); + + List values = property.getValues(); + for (String valueStr : valueStrs) { + V value = readValue(property, valueStr, dataType, parameters, context); + values.add(value); + } + + return property; + } + + protected abstract T newInstance(ICalDataType dataType, ICalParameters parameters); + + protected abstract String writeValue(T property, V value, WriteContext context); + + protected abstract V readValue(T property, String value, ICalDataType dataType, ICalParameters parameters, ParseContext context); +} diff --git a/app/src/main/java/biweekly/io/scribe/property/LocationScribe.java b/app/src/main/java/biweekly/io/scribe/property/LocationScribe.java new file mode 100644 index 0000000000..834cb0104e --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/LocationScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Location; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Location} properties. + * @author Michael Angstadt + */ +public class LocationScribe extends TextPropertyScribe { + public LocationScribe() { + super(Location.class, "LOCATION"); + } + + @Override + protected Location newInstance(String value, ICalVersion version) { + return new Location(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/MethodScribe.java b/app/src/main/java/biweekly/io/scribe/property/MethodScribe.java new file mode 100644 index 0000000000..e640df5293 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/MethodScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.Method; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Method} properties. + * @author Michael Angstadt + */ +public class MethodScribe extends TextPropertyScribe { + public MethodScribe() { + super(Method.class, "METHOD"); + } + + @Override + protected Method newInstance(String value, ICalVersion version) { + return new Method(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/NameScribe.java b/app/src/main/java/biweekly/io/scribe/property/NameScribe.java new file mode 100644 index 0000000000..02b0cc2c26 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/NameScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Name; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Name} properties. + * @author Michael Angstadt + */ +public class NameScribe extends TextPropertyScribe { + public NameScribe() { + super(Name.class, "NAME"); + } + + @Override + protected Name newInstance(String value, ICalVersion version) { + return new Name(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/OrganizerScribe.java b/app/src/main/java/biweekly/io/scribe/property/OrganizerScribe.java new file mode 100644 index 0000000000..155009d972 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/OrganizerScribe.java @@ -0,0 +1,118 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.DataModelConversionException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.parameter.ICalParameters; +import biweekly.parameter.Role; +import biweekly.property.Attendee; +import biweekly.property.Organizer; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Organizer} properties. + * @author Michael Angstadt + */ +public class OrganizerScribe extends ICalPropertyScribe { + public OrganizerScribe() { + super(Organizer.class, "ORGANIZER", ICalDataType.CAL_ADDRESS); + } + + @Override + protected ICalParameters _prepareParameters(Organizer property, WriteContext context) { + //CN parameter + String name = property.getCommonName(); + if (name != null) { + ICalParameters copy = new ICalParameters(property.getParameters()); + copy.put(ICalParameters.CN, name); + return copy; + } + + return super._prepareParameters(property, context); + } + + @Override + protected Organizer _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String name = parameters.getCommonName(); + if (name != null) { + parameters.remove(ICalParameters.CN, name); + } + + String uri = null, email = null; + int colon = value.indexOf(':'); + if (colon == 6) { + String scheme = value.substring(0, colon); + if (scheme.equalsIgnoreCase("mailto")) { + email = value.substring(colon + 1); + } else { + uri = value; + } + } else { + uri = value; + } + + Organizer organizer = new Organizer(name, email); + organizer.setUri(uri); + return organizer; + } + + @Override + protected String _writeText(Organizer property, WriteContext context) { + if (context.getVersion() == ICalVersion.V1_0) { + Attendee attendee = new Attendee(property.getCommonName(), property.getEmail()); + attendee.setRole(Role.ORGANIZER); + attendee.setUri(property.getUri()); + attendee.setParameters(property.getParameters()); + + DataModelConversionException conversionException = new DataModelConversionException(property); + conversionException.getProperties().add(attendee); + throw conversionException; + } + + String uri = property.getUri(); + if (uri != null) { + return uri; + } + + String email = property.getEmail(); + if (email != null) { + return "mailto:" + email; + } + + return ""; + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/PercentCompleteScribe.java b/app/src/main/java/biweekly/io/scribe/property/PercentCompleteScribe.java new file mode 100644 index 0000000000..6461bb2a0b --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/PercentCompleteScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.PercentComplete; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link PercentComplete} properties. + * @author Michael Angstadt + */ +public class PercentCompleteScribe extends IntegerPropertyScribe { + public PercentCompleteScribe() { + super(PercentComplete.class, "PERCENT-COMPLETE"); + } + + @Override + protected PercentComplete newInstance(Integer value) { + return new PercentComplete(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/PriorityScribe.java b/app/src/main/java/biweekly/io/scribe/property/PriorityScribe.java new file mode 100644 index 0000000000..a5705b2769 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/PriorityScribe.java @@ -0,0 +1,43 @@ +package biweekly.io.scribe.property; + +import biweekly.property.Priority; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Priority} properties. + * @author Michael Angstadt + */ +public class PriorityScribe extends IntegerPropertyScribe { + public PriorityScribe() { + super(Priority.class, "PRIORITY"); + } + + @Override + protected Priority newInstance(Integer value) { + return new Priority(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/ProcedureAlarmScribe.java b/app/src/main/java/biweekly/io/scribe/property/ProcedureAlarmScribe.java new file mode 100644 index 0000000000..59085f3992 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ProcedureAlarmScribe.java @@ -0,0 +1,67 @@ +package biweekly.io.scribe.property; + +import java.util.Collections; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.component.VAlarm; +import biweekly.property.Action; +import biweekly.property.ProcedureAlarm; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link ProcedureAlarm} properties. + * @author Michael Angstadt + */ +public class ProcedureAlarmScribe extends VCalAlarmPropertyScribe { + public ProcedureAlarmScribe() { + super(ProcedureAlarm.class, "PALARM", ICalDataType.TEXT); + } + + @Override + protected List writeData(ProcedureAlarm property) { + String path = property.getPath(); + return (path == null) ? Collections.emptyList() : Collections.singletonList(path); + } + + @Override + protected ProcedureAlarm create(ICalDataType dataType, SemiStructuredValueIterator it) { + return new ProcedureAlarm(it.next()); + } + + @Override + protected void toVAlarm(VAlarm valarm, ProcedureAlarm property) { + valarm.setDescription(property.getPath()); + } + + @Override + protected Action action() { + return Action.procedure(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/ProductIdScribe.java b/app/src/main/java/biweekly/io/scribe/property/ProductIdScribe.java new file mode 100644 index 0000000000..d4703a6d89 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ProductIdScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.ProductId; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link ProductId} properties. + * @author Michael Angstadt + */ +public class ProductIdScribe extends TextPropertyScribe { + public ProductIdScribe() { + super(ProductId.class, "PRODID"); + } + + @Override + protected ProductId newInstance(String value, ICalVersion version) { + return new ProductId(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/RawPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/RawPropertyScribe.java new file mode 100644 index 0000000000..f7482dc2cb --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RawPropertyScribe.java @@ -0,0 +1,117 @@ +package biweekly.io.scribe.property; + +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.json.JsonValue; +import biweekly.io.xml.XCalElement; +import biweekly.io.xml.XCalElement.XCalValue; +import biweekly.parameter.ICalParameters; +import biweekly.property.RawProperty; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RawProperty} properties. + * @author Michael Angstadt + */ +/* + * Note concerning escaping and unescaping special characters: + * + * Values are not escaped and unescaped for the following reason: If the + * experimental property's value is a list or structured list, then the escaping + * must be preserved or else escaped special characters will be lost. + * + * This is an inconvenience, considering the fact that most experimental + * properties contain simple text values. But it is necessary in order to + * prevent data loss. + */ +public class RawPropertyScribe extends ICalPropertyScribe { + public RawPropertyScribe(String propertyName) { + super(RawProperty.class, propertyName, null); + } + + @Override + protected ICalDataType _dataType(RawProperty property, ICalVersion version) { + return property.getDataType(); + } + + @Override + protected String _writeText(RawProperty property, WriteContext context) { + String value = property.getValue(); + return (value == null) ? "" : value; + } + + @Override + protected RawProperty _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return new RawProperty(propertyName, dataType, value); + } + + @Override + protected RawProperty _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + XCalValue firstValue = element.firstValue(); + ICalDataType dataType = firstValue.getDataType(); + String value = firstValue.getValue(); + + return new RawProperty(propertyName, dataType, value); + } + + @Override + protected RawProperty _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = jcardValueToString(value); + + return new RawProperty(propertyName, dataType, valueStr); + } + + private static String jcardValueToString(JCalValue value) { + /* + * ICalPropertyScribe.jcardValueToString() cannot be used because it + * escapes single values. + */ + List values = value.getValues(); + if (values.size() > 1) { + List multi = value.asMulti(); + if (!multi.isEmpty()) { + return VObjectPropertyValues.writeList(multi); + } + } + + if (!values.isEmpty() && values.get(0).getArray() != null) { + List> structured = value.asStructured(); + if (!structured.isEmpty()) { + return VObjectPropertyValues.writeStructured(structured, true); + } + } + + return value.asSingle(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/RecurrenceDatesScribe.java b/app/src/main/java/biweekly/io/scribe/property/RecurrenceDatesScribe.java new file mode 100644 index 0000000000..8d63a36a2e --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RecurrenceDatesScribe.java @@ -0,0 +1,377 @@ +package biweekly.io.scribe.property; + +import static biweekly.ICalDataType.DATE; +import static biweekly.ICalDataType.DATE_TIME; +import static biweekly.ICalDataType.PERIOD; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.RecurrenceDates; +import biweekly.util.Duration; +import biweekly.util.ICalDate; +import biweekly.util.Period; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RecurrenceDates} properties. + * @author Michael Angstadt + */ +public class RecurrenceDatesScribe extends ICalPropertyScribe { + public RecurrenceDatesScribe() { + super(RecurrenceDates.class, "RDATE", DATE_TIME); + } + + @Override + protected ICalParameters _prepareParameters(RecurrenceDates property, WriteContext context) { + if (isInObservance(context)) { + return property.getParameters(); + } + + List periods = property.getPeriods(); + List dates = property.getDates(); + boolean hasTime; + if (periods.isEmpty() && dates.isEmpty()) { + hasTime = false; + } else { + ICalDataType dataType = dataType(property, context.getVersion()); + hasTime = (dataType == DATE_TIME || dataType == PERIOD); + } + return handleTzidParameter(property, hasTime, context); + } + + @Override + protected ICalDataType _dataType(RecurrenceDates property, ICalVersion version) { + List dates = property.getDates(); + if (!dates.isEmpty()) { + return dates.get(0).hasTime() ? DATE_TIME : DATE; + } + + if (!property.getPeriods().isEmpty()) { + return PERIOD; + } + + return defaultDataType(version); + } + + @Override + protected String _writeText(final RecurrenceDates property, final WriteContext context) { + List dates = property.getDates(); + if (!dates.isEmpty()) { + boolean inObservance = isInObservance(context); + List values = new ArrayList(dates.size()); + for (ICalDate date : dates) { + String value; + if (inObservance) { + value = date(date).observance(true).extended(false).write(); + } else { + value = date(date, property, context).extended(false).write(); + } + values.add(value); + } + return VObjectPropertyValues.writeList(values); + } + + //TODO vCal does not support periods + List periods = property.getPeriods(); + if (!periods.isEmpty()) { + List values = new ArrayList(periods.size()); + for (Period period : periods) { + StringBuilder sb = new StringBuilder(); + + Date start = period.getStartDate(); + if (start != null) { + String date = date(start, property, context).extended(false).write(); + sb.append(date); + } + + sb.append('/'); + + Date end = period.getEndDate(); + Duration duration = period.getDuration(); + if (end != null) { + String date = date(end, property, context).extended(false).write(); + sb.append(date); + } else if (duration != null) { + sb.append(duration); + } + + values.add(sb.toString()); + } + return VObjectPropertyValues.writeList(values); + } + + return ""; + } + + @Override + protected RecurrenceDates _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(VObjectPropertyValues.parseList(value), dataType, parameters, context); + } + + @Override + protected void _writeXml(RecurrenceDates property, XCalElement element, WriteContext context) { + ICalDataType dataType = dataType(property, context.getVersion()); + List dates = property.getDates(); + if (!dates.isEmpty()) { + boolean inObservance = isInObservance(context); + for (ICalDate date : dates) { + String dateStr; + if (inObservance) { + dateStr = date(date).observance(true).extended(true).write(); + } else { + dateStr = date(date, property, context).extended(true).write(); + } + + element.append(dataType, dateStr); + } + return; + } + + List periods = property.getPeriods(); + if (!periods.isEmpty()) { + for (Period period : periods) { + XCalElement periodElement = element.append(dataType); + + Date start = period.getStartDate(); + if (start != null) { + String dateStr = date(start, property, context).extended(true).write(); + periodElement.append("start", dateStr); + } + + Date end = period.getEndDate(); + if (end != null) { + String dateStr = date(end, property, context).extended(true).write(); + periodElement.append("end", dateStr); + } + + Duration duration = period.getDuration(); + if (duration != null) { + periodElement.append("duration", duration.toString()); + } + } + return; + } + + element.append(defaultDataType(context.getVersion()), ""); + } + + @Override + protected RecurrenceDates _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + List periodElements = element.children(PERIOD); + List dateTimeElements = element.all(DATE_TIME); + List dateElements = element.all(DATE); + if (periodElements.isEmpty() && dateTimeElements.isEmpty() && dateElements.isEmpty()) { + throw missingXmlElements(PERIOD, DATE_TIME, DATE); + } + + RecurrenceDates property = new RecurrenceDates(); + + //parse periods + for (XCalElement periodElement : periodElements) { + String startStr = periodElement.first("start"); + if (startStr == null) { + throw new CannotParseException(9); + } + + ICalDate start; + try { + start = date(startStr).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(10, startStr); + } + + String endStr = periodElement.first("end"); + if (endStr != null) { + try { + ICalDate end = date(endStr).parse(); + property.getPeriods().add(new Period(start, end)); + context.addDate(start, property, parameters); + context.addDate(end, property, parameters); + } catch (IllegalArgumentException e) { + throw new CannotParseException(11, endStr); + } + continue; + } + + String durationStr = periodElement.first("duration"); + if (durationStr != null) { + try { + Duration duration = Duration.parse(durationStr); + property.getPeriods().add(new Period(start, duration)); + context.addDate(start, property, parameters); + } catch (IllegalArgumentException e) { + throw new CannotParseException(12, durationStr); + } + continue; + } + + throw new CannotParseException(13); + } + + //parse date-times + for (String dateTimeStr : dateTimeElements) { + try { + ICalDate date = date(dateTimeStr).hasTime(true).parse(); + property.getDates().add(date); + context.addDate(date, property, parameters); + } catch (IllegalArgumentException e) { + throw new CannotParseException(15, dateTimeStr); + } + } + + //parse dates + for (String dateStr : dateElements) { + try { + ICalDate date = date(dateStr).hasTime(false).parse(); + property.getDates().add(date); + } catch (IllegalArgumentException e) { + throw new CannotParseException(15, dateStr); + } + } + + return property; + } + + @Override + protected JCalValue _writeJson(RecurrenceDates property, WriteContext context) { + List values = new ArrayList(); + List dates = property.getDates(); + List periods = property.getPeriods(); + if (!dates.isEmpty()) { + boolean inObservance = isInObservance(context); + for (ICalDate date : dates) { + String dateStr; + if (inObservance) { + dateStr = date(date).observance(true).extended(true).write(); + } else { + dateStr = date(date, property, context).extended(true).write(); + } + + values.add(dateStr); + } + } else if (!periods.isEmpty()) { + for (Period period : property.getPeriods()) { + StringBuilder sb = new StringBuilder(); + Date start = period.getStartDate(); + if (start != null) { + String dateStr = date(start, property, context).extended(true).write(); + sb.append(dateStr); + } + + sb.append('/'); + + Date end = period.getEndDate(); + Duration duration = period.getDuration(); + if (end != null) { + String dateStr = date(end, property, context).extended(true).write(); + sb.append(dateStr); + } else if (duration != null) { + sb.append(duration); + } + + values.add(sb.toString()); + } + } + + if (values.isEmpty()) { + values.add(""); + } + return JCalValue.multi(values); + } + + @Override + protected RecurrenceDates _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(value.asMulti(), dataType, parameters, context); + } + + private RecurrenceDates parse(List valueStrs, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + RecurrenceDates property = new RecurrenceDates(); + + if (dataType == PERIOD) { + //parse as periods + for (String timePeriodStr : valueStrs) { + int slash = timePeriodStr.indexOf('/'); + if (slash < 0) { + throw new CannotParseException(13); + } + + String startStr = timePeriodStr.substring(0, slash); + ICalDate start; + try { + start = date(startStr).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(10, startStr); + } + + String endStr = timePeriodStr.substring(slash + 1); + ICalDate end; + try { + end = date(endStr).parse(); + property.getPeriods().add(new Period(start, end)); + context.addDate(start, property, parameters); + context.addDate(end, property, parameters); + } catch (IllegalArgumentException e) { + //must be a duration + try { + Duration duration = Duration.parse(endStr); + property.getPeriods().add(new Period(start, duration)); + context.addDate(start, property, parameters); + } catch (IllegalArgumentException e2) { + throw new CannotParseException(14, endStr); + } + } + } + return property; + } + + //parse as dates + boolean hasTime = (dataType == DATE_TIME); + for (String valueStr : valueStrs) { + ICalDate date; + try { + date = date(valueStr).hasTime(hasTime).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(15, valueStr); + } + property.getDates().add(date); + context.addDate(date, property, parameters); + } + return property; + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/RecurrenceIdScribe.java b/app/src/main/java/biweekly/io/scribe/property/RecurrenceIdScribe.java new file mode 100644 index 0000000000..462c127588 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RecurrenceIdScribe.java @@ -0,0 +1,53 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.RecurrenceId; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RecurrenceId} properties. + * @author Michael Angstadt + */ +public class RecurrenceIdScribe extends DateOrDateTimePropertyScribe { + public RecurrenceIdScribe() { + super(RecurrenceId.class, "RECURRENCE-ID"); + } + + @Override + protected RecurrenceId newInstance(ICalDate date) { + return new RecurrenceId(date); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java new file mode 100644 index 0000000000..a27742a93f --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RecurrencePropertyScribe.java @@ -0,0 +1,1012 @@ +package biweekly.io.scribe.property; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.w3c.dom.Element; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.component.ICalComponent; +import biweekly.io.CannotParseException; +import biweekly.io.DataModelConversionException; +import biweekly.io.ParseContext; +import biweekly.io.ParseWarning; +import biweekly.io.TimezoneInfo; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.io.xml.XCalNamespaceContext; +import biweekly.parameter.ICalParameters; +import biweekly.property.DateStart; +import biweekly.property.ICalProperty; +import biweekly.property.RawProperty; +import biweekly.property.RecurrenceProperty; +import biweekly.util.ByDay; +import biweekly.util.DayOfWeek; +import biweekly.util.Frequency; +import biweekly.util.ICalDate; +import biweekly.util.ListMultimap; +import biweekly.util.Recurrence; +import biweekly.util.XmlUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties whose values are {@link Recurrence}. + * @param the property class + * @author Michael Angstadt + */ +public abstract class RecurrencePropertyScribe extends ICalPropertyScribe { + private static final String FREQ = "FREQ"; + private static final String UNTIL = "UNTIL"; + private static final String COUNT = "COUNT"; + private static final String INTERVAL = "INTERVAL"; + private static final String BYSECOND = "BYSECOND"; + private static final String BYMINUTE = "BYMINUTE"; + private static final String BYHOUR = "BYHOUR"; + private static final String BYDAY = "BYDAY"; + private static final String BYMONTHDAY = "BYMONTHDAY"; + private static final String BYYEARDAY = "BYYEARDAY"; + private static final String BYWEEKNO = "BYWEEKNO"; + private static final String BYMONTH = "BYMONTH"; + private static final String BYSETPOS = "BYSETPOS"; + private static final String WKST = "WKST"; + + public RecurrencePropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + return ICalDataType.RECUR; + } + + @Override + protected String _writeText(T property, WriteContext context) { + //null value + Recurrence recur = property.getValue(); + if (recur == null) { + return ""; + } + + switch (context.getVersion()) { + case V1_0: + return writeTextV1(property, context); + default: + return writeTextV2(property, context); + } + } + + private String writeTextV1(T property, WriteContext context) { + Recurrence recur = property.getValue(); + Frequency frequency = recur.getFrequency(); + if (frequency == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + Integer interval = recur.getInterval(); + if (interval == null) { + interval = 1; + } + + switch (frequency) { + case YEARLY: + if (!recur.getByMonth().isEmpty()) { + sb.append("YM").append(interval); + for (Integer month : recur.getByMonth()) { + sb.append(' ').append(month); + } + } else { + sb.append("YD").append(interval); + for (Integer day : recur.getByYearDay()) { + sb.append(' ').append(day); + } + } + break; + + case MONTHLY: + if (!recur.getByMonthDay().isEmpty()) { + sb.append("MD").append(interval); + for (Integer day : recur.getByMonthDay()) { + sb.append(' ').append(writeVCalInt(day)); + } + } else { + sb.append("MP").append(interval); + for (ByDay byDay : recur.getByDay()) { + DayOfWeek day = byDay.getDay(); + Integer prefix = byDay.getNum(); + if (prefix == null) { + prefix = 1; + } + + sb.append(' ').append(writeVCalInt(prefix)).append(' ').append(day.getAbbr()); + } + } + break; + + case WEEKLY: + sb.append("W").append(interval); + for (ByDay byDay : recur.getByDay()) { + sb.append(' ').append(byDay.getDay().getAbbr()); + } + break; + + case DAILY: + sb.append("D").append(interval); + break; + + case HOURLY: + sb.append("M").append(interval * 60); + break; + + case MINUTELY: + sb.append("M").append(interval); + break; + + default: + return ""; + } + + Integer count = recur.getCount(); + ICalDate until = recur.getUntil(); + sb.append(' '); + + if (count != null) { + sb.append('#').append(count); + } else if (until != null) { + String dateStr = date(until, property, context).extended(false).write(); + sb.append(dateStr); + } else { + sb.append("#0"); + } + + return sb.toString(); + } + + private String writeTextV2(T property, WriteContext context) { + ListMultimap components = buildComponents(property, context, false); + return VObjectPropertyValues.writeMultimap(components.getMap()); + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + if (value.isEmpty()) { + return newInstance(new Recurrence.Builder((Frequency) null).build()); + } + + switch (context.getVersion()) { + case V1_0: + handleVersion1Multivalued(value, dataType, parameters, context); + return parseTextV1(value, dataType, parameters, context); + default: + return parseTextV2(value, dataType, parameters, context); + } + } + + /** + * Version 1.0 allows multiple RRULE values to be defined inside of the same + * property. This method checks for this and, if multiple values are found, + * parses them and throws a {@link DataModelConversionException}. + * @param value the property value + * @param dataType the property data type + * @param parameters the property parameters + * @param context the parse context + * @throws DataModelConversionException if the property contains multiple + * RRULE values + */ + private void handleVersion1Multivalued(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + List rrules = splitRRULEValues(value); + if (rrules.size() == 1) { + return; + } + + DataModelConversionException conversionException = new DataModelConversionException(null); + for (String rrule : rrules) { + ICalParameters parametersCopy = new ICalParameters(parameters); + + ICalProperty property; + try { + property = parseTextV1(rrule, dataType, parametersCopy, context); + } catch (CannotParseException e) { + //@formatter:off + context.getWarnings().add(new ParseWarning.Builder(context) + .message(e) + .build() + ); + //@formatter:on + property = new RawProperty(getPropertyName(context.getVersion()), dataType, rrule); + property.setParameters(parametersCopy); + } + conversionException.getProperties().add(property); + } + + throw conversionException; + } + + /** + * Version 1.0 allows multiple RRULE values to be defined inside of the same + * property. This method extracts each RRULE value from the property value. + * @param value the property value + * @return the RRULE values + */ + private List splitRRULEValues(String value) { + List values = new ArrayList(); + Pattern p = Pattern.compile("#\\d+|\\d{8}T\\d{6}Z?"); + Matcher m = p.matcher(value); + + int prevIndex = 0; + while (m.find()) { + int end = m.end(); + String subValue = value.substring(prevIndex, end).trim(); + values.add(subValue); + prevIndex = end; + } + String subValue = value.substring(prevIndex).trim(); + if (subValue.length() > 0) { + values.add(subValue); + } + + return values; + } + + private T parseTextV1(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + final Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); + + List splitValues = Arrays.asList(value.toUpperCase().split("\\s+")); + + //parse the frequency and interval from the first token (e.g. "W2") + String frequencyStr; + Integer interval; + { + String firstToken = splitValues.get(0); + Pattern p = Pattern.compile("^([A-Z]+)(\\d+)$"); + Matcher m = p.matcher(firstToken); + if (!m.find()) { + throw new CannotParseException(40, firstToken); + } + + frequencyStr = m.group(1); + interval = integerValueOf(m.group(2)); + + splitValues = splitValues.subList(1, splitValues.size()); + } + builder.interval(interval); + + Integer count = null; + ICalDate until = null; + if (splitValues.isEmpty()) { + count = 2; + } else { + String lastToken = splitValues.get(splitValues.size() - 1); + if (lastToken.startsWith("#")) { + String countStr = lastToken.substring(1); + count = integerValueOf(countStr); + if (count == 0) { + //infinite + count = null; + } + + splitValues = splitValues.subList(0, splitValues.size() - 1); + } else { + try { + //see if the value is an "until" date + until = date(lastToken).parse(); + splitValues = splitValues.subList(0, splitValues.size() - 1); + } catch (IllegalArgumentException e) { + //last token is a regular value + count = 2; + } + } + } + builder.count(count); + builder.until(until); + + //determine what frequency enum to use and how to treat each tokenized value + Frequency frequency; + Handler handler; + if ("YD".equals(frequencyStr)) { + frequency = Frequency.YEARLY; + handler = new Handler() { + public void handle(String value) { + if (value == null) { + return; + } + + Integer dayOfYear = integerValueOf(value); + builder.byYearDay(dayOfYear); + } + }; + } else if ("YM".equals(frequencyStr)) { + frequency = Frequency.YEARLY; + handler = new Handler() { + public void handle(String value) { + if (value == null) { + return; + } + + Integer month = integerValueOf(value); + builder.byMonth(month); + } + }; + } else if ("MD".equals(frequencyStr)) { + frequency = Frequency.MONTHLY; + handler = new Handler() { + public void handle(String value) { + if (value == null) { + return; + } + + try { + Integer date = "LD".equals(value) ? -1 : parseVCalInt(value); + builder.byMonthDay(date); + } catch (NumberFormatException e) { + throw new CannotParseException(40, value); + } + } + }; + } else if ("MP".equals(frequencyStr)) { + frequency = Frequency.MONTHLY; + handler = new Handler() { + private final List nums = new ArrayList(); + private final List days = new ArrayList(); + private boolean readNum = false; + + public void handle(String value) { + if (value == null) { + //end of list + for (Integer num : nums) { + for (DayOfWeek day : days) { + builder.byDay(num, day); + } + } + return; + } + + if (value.matches("\\d{4}")) { + readNum = false; + + Integer hour = integerValueOf(value.substring(0, 2)); + builder.byHour(hour); + + Integer minute = integerValueOf(value.substring(2, 4)); + builder.byMinute(minute); + return; + } + + try { + Integer curNum = parseVCalInt(value); + + if (!readNum) { + //reset lists, new segment + for (Integer num : nums) { + for (DayOfWeek day : days) { + builder.byDay(num, day); + } + } + nums.clear(); + days.clear(); + + readNum = true; + } + + nums.add(curNum); + } catch (NumberFormatException e) { + readNum = false; + + DayOfWeek day = parseDay(value); + days.add(day); + } + } + }; + } else if ("W".equals(frequencyStr)) { + frequency = Frequency.WEEKLY; + handler = new Handler() { + public void handle(String value) { + if (value == null) { + return; + } + + DayOfWeek day = parseDay(value); + builder.byDay(day); + } + }; + } else if ("D".equals(frequencyStr)) { + frequency = Frequency.DAILY; + handler = new Handler() { + public void handle(String value) { + if (value == null) { + return; + } + + Integer hour = integerValueOf(value.substring(0, 2)); + builder.byHour(hour); + + Integer minute = integerValueOf(value.substring(2, 4)); + builder.byMinute(minute); + } + }; + } else if ("M".equals(frequencyStr)) { + frequency = Frequency.MINUTELY; + handler = new Handler() { + public void handle(String value) { + //TODO can this ever have values? + } + }; + } else { + throw new CannotParseException(41, frequencyStr); + } + + builder.frequency(frequency); + + //parse the rest of the tokens + for (String splitValue : splitValues) { + //TODO not sure how to handle the "$" symbol, ignore it + if (splitValue.endsWith("$")) { + context.addWarning(36, splitValue); + splitValue = splitValue.substring(0, splitValue.length() - 1); + } + + handler.handle(splitValue); + } + handler.handle(null); + + T property = newInstance(builder.build()); + if (until != null) { + context.addDate(until, property, parameters); + } + + return property; + } + + private T parseTextV2(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); + ListMultimap rules = new ListMultimap(VObjectPropertyValues.parseMultimap(value)); + + parseFreq(rules, builder, context); + parseUntil(rules, builder, context); + parseCount(rules, builder, context); + parseInterval(rules, builder, context); + parseBySecond(rules, builder, context); + parseByMinute(rules, builder, context); + parseByHour(rules, builder, context); + parseByDay(rules, builder, context); + parseByMonthDay(rules, builder, context); + parseByYearDay(rules, builder, context); + parseByWeekNo(rules, builder, context); + parseByMonth(rules, builder, context); + parseBySetPos(rules, builder, context); + parseWkst(rules, builder, context); + parseXRules(rules, builder); //must be called last + + T property = newInstance(builder.build()); + + ICalDate until = property.getValue().getUntil(); + if (until != null) { + context.addDate(until, property, parameters); + } + + return property; + } + + /** + * Parses an integer string, where the sign is at the end of the string + * instead of at the beginning (for example, "5-"). + * @param value the string + * @return the value + * @throws NumberFormatException if the string cannot be parsed as an + * integer + */ + private static int parseVCalInt(String value) { + int negate = 1; + if (value.endsWith("+")) { + value = value.substring(0, value.length() - 1); + } else if (value.endsWith("-")) { + value = value.substring(0, value.length() - 1); + negate = -1; + } + + return Integer.parseInt(value) * negate; + } + + /** + * Same as {@link Integer#valueOf(String)}, but throws a + * {@link CannotParseException} when it fails. + * @param value the string to parse + * @return the parse integer + * @throws CannotParseException if the string cannot be parsed + */ + private static Integer integerValueOf(String value) { + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + throw new CannotParseException(40, value); + } + } + + private static String writeVCalInt(Integer value) { + if (value > 0) { + return value + "+"; + } + + if (value < 0) { + return Math.abs(value) + "-"; + } + + return value.toString(); + } + + private DayOfWeek parseDay(String value) { + DayOfWeek day = DayOfWeek.valueOfAbbr(value); + if (day == null) { + throw new CannotParseException(42, value); + } + + return day; + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + XCalElement recurElement = element.append(dataType(property, null)); + + Recurrence recur = property.getValue(); + if (recur == null) { + return; + } + + ListMultimap components = buildComponents(property, context, true); + for (Map.Entry> component : components) { + String name = component.getKey().toLowerCase(); + for (Object value : component.getValue()) { + recurElement.append(name, value.toString()); + } + } + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + XCalElement value = element.child(dataType); + if (value == null) { + throw missingXmlElements(dataType); + } + + ListMultimap rules = new ListMultimap(); + for (Element child : XmlUtils.toElementList(value.getElement().getChildNodes())) { + if (!XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) { + continue; + } + + String name = child.getLocalName().toUpperCase(); + String text = child.getTextContent(); + rules.put(name, text); + } + + Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); + + parseFreq(rules, builder, context); + parseUntil(rules, builder, context); + parseCount(rules, builder, context); + parseInterval(rules, builder, context); + parseBySecond(rules, builder, context); + parseByMinute(rules, builder, context); + parseByHour(rules, builder, context); + parseByDay(rules, builder, context); + parseByMonthDay(rules, builder, context); + parseByYearDay(rules, builder, context); + parseByWeekNo(rules, builder, context); + parseByMonth(rules, builder, context); + parseBySetPos(rules, builder, context); + parseWkst(rules, builder, context); + parseXRules(rules, builder); //must be called last + + T property = newInstance(builder.build()); + + ICalDate until = property.getValue().getUntil(); + if (until != null) { + context.addDate(until, property, parameters); + } + + return property; + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + Recurrence recur = property.getValue(); + if (recur == null) { + return JCalValue.object(new ListMultimap(0)); + } + + ListMultimap components = buildComponents(property, context, true); + + //lower-case all the keys + ListMultimap object = new ListMultimap(components.keySet().size()); + for (Map.Entry> entry : components) { + String key = entry.getKey().toLowerCase(); + object.putAll(key, entry.getValue()); + } + + return JCalValue.object(object); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); + + //upper-case the keys + ListMultimap object = value.asObject(); + ListMultimap rules = new ListMultimap(object.keySet().size()); + for (Map.Entry> entry : object) { + String key = entry.getKey().toUpperCase(); + rules.putAll(key, entry.getValue()); + } + + parseFreq(rules, builder, context); + parseUntil(rules, builder, context); + parseCount(rules, builder, context); + parseInterval(rules, builder, context); + parseBySecond(rules, builder, context); + parseByMinute(rules, builder, context); + parseByHour(rules, builder, context); + parseByDay(rules, builder, context); + parseByMonthDay(rules, builder, context); + parseByYearDay(rules, builder, context); + parseByWeekNo(rules, builder, context); + parseByMonth(rules, builder, context); + parseBySetPos(rules, builder, context); + parseWkst(rules, builder, context); + parseXRules(rules, builder); //must be called last + + T property = newInstance(builder.build()); + + ICalDate until = property.getValue().getUntil(); + if (until != null) { + context.addDate(until, property, parameters); + } + + return property; + } + + /** + * Creates a new instance of the recurrence property. + * @param recur the recurrence value + * @return the new instance + */ + protected abstract T newInstance(Recurrence recur); + + private void parseFreq(ListMultimap rules, final Recurrence.Builder builder, final ParseContext context) { + parseFirst(rules, FREQ, new Handler() { + public void handle(String value) { + value = value.toUpperCase(); + try { + builder.frequency(Frequency.valueOf(value)); + } catch (IllegalArgumentException e) { + context.addWarning(7, FREQ, value); + } + } + }); + } + + private void parseUntil(ListMultimap rules, final Recurrence.Builder builder, final ParseContext context) { + parseFirst(rules, UNTIL, new Handler() { + public void handle(String value) { + try { + builder.until(date(value).parse()); + } catch (IllegalArgumentException e) { + context.addWarning(7, UNTIL, value); + } + } + }); + } + + private void parseCount(ListMultimap rules, final Recurrence.Builder builder, final ParseContext context) { + parseFirst(rules, COUNT, new Handler() { + public void handle(String value) { + try { + builder.count(Integer.valueOf(value)); + } catch (NumberFormatException e) { + context.addWarning(7, COUNT, value); + } + } + }); + } + + private void parseInterval(ListMultimap rules, final Recurrence.Builder builder, final ParseContext context) { + parseFirst(rules, INTERVAL, new Handler() { + public void handle(String value) { + try { + builder.interval(Integer.valueOf(value)); + } catch (NumberFormatException e) { + context.addWarning(7, INTERVAL, value); + } + } + }); + } + + private void parseBySecond(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYSECOND, rules, context, new Handler() { + public void handle(Integer value) { + builder.bySecond(value); + } + }); + } + + private void parseByMinute(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYMINUTE, rules, context, new Handler() { + public void handle(Integer value) { + builder.byMinute(value); + } + }); + } + + private void parseByHour(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYHOUR, rules, context, new Handler() { + public void handle(Integer value) { + builder.byHour(value); + } + }); + } + + private void parseByDay(ListMultimap rules, Recurrence.Builder builder, ParseContext context) { + Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$"); + for (String value : rules.removeAll(BYDAY)) { + Matcher m = p.matcher(value); + if (!m.find()) { + //this should never happen + //the regex contains a "match-all" pattern and should never not find anything + context.addWarning(7, BYDAY, value); + continue; + } + + String dayStr = m.group(2); + DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr); + if (day == null) { + context.addWarning(7, BYDAY, value); + continue; + } + + String prefixStr = m.group(1); + Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr); //no need to catch NumberFormatException because the regex guarantees that it will be a number + + builder.byDay(prefix, day); + } + } + + private void parseByMonthDay(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYMONTHDAY, rules, context, new Handler() { + public void handle(Integer value) { + builder.byMonthDay(value); + } + }); + } + + private void parseByYearDay(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYYEARDAY, rules, context, new Handler() { + public void handle(Integer value) { + builder.byYearDay(value); + } + }); + } + + private void parseByWeekNo(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYWEEKNO, rules, context, new Handler() { + public void handle(Integer value) { + builder.byWeekNo(value); + } + }); + } + + private void parseByMonth(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYMONTH, rules, context, new Handler() { + public void handle(Integer value) { + builder.byMonth(value); + } + }); + } + + private void parseBySetPos(ListMultimap rules, final Recurrence.Builder builder, ParseContext context) { + parseIntegerList(BYSETPOS, rules, context, new Handler() { + public void handle(Integer value) { + builder.bySetPos(value); + } + }); + } + + private void parseWkst(ListMultimap rules, final Recurrence.Builder builder, final ParseContext context) { + parseFirst(rules, WKST, new Handler() { + public void handle(String value) { + DayOfWeek day = DayOfWeek.valueOfAbbr(value); + if (day == null) { + context.addWarning(7, WKST, value); + return; + } + + builder.workweekStarts(day); + } + }); + } + + private void parseXRules(ListMultimap rules, Recurrence.Builder builder) { + for (Map.Entry> rule : rules) { + String name = rule.getKey(); + for (String value : rule.getValue()) { + builder.xrule(name, value); + } + } + } + + private ListMultimap buildComponents(T property, WriteContext context, boolean extended) { + ListMultimap components = new ListMultimap(); + Recurrence recur = property.getValue(); + + //FREQ must come first + if (recur.getFrequency() != null) { + components.put(FREQ, recur.getFrequency().name()); + } + + ICalDate until = recur.getUntil(); + if (until != null) { + components.put(UNTIL, writeUntil(until, context, extended)); + } + + if (recur.getCount() != null) { + components.put(COUNT, recur.getCount()); + } + + if (recur.getInterval() != null) { + components.put(INTERVAL, recur.getInterval()); + } + + components.putAll(BYSECOND, recur.getBySecond()); + components.putAll(BYMINUTE, recur.getByMinute()); + components.putAll(BYHOUR, recur.getByHour()); + + for (ByDay byDay : recur.getByDay()) { + Integer prefix = byDay.getNum(); + DayOfWeek day = byDay.getDay(); + + String value = day.getAbbr(); + if (prefix != null) { + value = prefix + value; + } + components.put(BYDAY, value); + } + + components.putAll(BYMONTHDAY, recur.getByMonthDay()); + components.putAll(BYYEARDAY, recur.getByYearDay()); + components.putAll(BYWEEKNO, recur.getByWeekNo()); + components.putAll(BYMONTH, recur.getByMonth()); + components.putAll(BYSETPOS, recur.getBySetPos()); + + if (recur.getWorkweekStarts() != null) { + components.put(WKST, recur.getWorkweekStarts().getAbbr()); + } + + for (Map.Entry> entry : recur.getXRules().entrySet()) { + String name = entry.getKey(); + List values = entry.getValue(); + components.putAll(name, values); + } + + return components; + } + + private String writeUntil(ICalDate until, WriteContext context, boolean extended) { + if (!until.hasTime()) { + return date(until).extended(extended).write(); + } + + /* + * RFC 5545 p.41 + * + * In the case of the "STANDARD" and "DAYLIGHT" sub-components the UNTIL + * rule part MUST always be specified as a date with UTC time. If + * specified as a DATE-TIME value, then it MUST be specified in a UTC + * time format. + */ + + if (isInObservance(context)) { + return date(until).utc(true).extended(extended).write(); + } + + /* + * RFC 2445 p.42 + * + * If specified as a date-time value, then it MUST be specified in an + * UTC time format. + */ + if (context.getVersion() == ICalVersion.V2_0_DEPRECATED) { + return date(until).extended(extended).utc(true).write(); + } + + /* + * RFC 5545 p.41 + * + * Furthermore, if the "DTSTART" property is specified as a date with + * local time, then the UNTIL rule part MUST also be specified as a date + * with local time. If the "DTSTART" property is specified as a date + * with UTC time or a date with local time and time zone reference, then + * the UNTIL rule part MUST be specified as a date with UTC time. + */ + + ICalComponent parent = context.getParent(); + if (parent == null) { + return date(until).extended(extended).utc(true).write(); + } + + DateStart dtstart = parent.getProperty(DateStart.class); + if (dtstart == null) { + return date(until).extended(extended).utc(true).write(); + } + + /* + * If DTSTART is floating, then UNTIL should be floating. + */ + TimezoneInfo tzinfo = context.getTimezoneInfo(); + boolean dtstartFloating = tzinfo.isFloating(dtstart); + if (dtstartFloating) { + return date(until).extended(extended).tz(true, null).write(); + } + + /* + * Otherwise, UNTIL should be UTC. + */ + return date(until).extended(extended).utc(true).write(); + } + + private void parseFirst(ListMultimap rules, String name, Handler handler) { + List values = rules.removeAll(name); + if (values.isEmpty()) { + return; + } + + String value = values.get(0); + handler.handle(value); + } + + private void parseIntegerList(String name, ListMultimap rules, ParseContext context, Handler handler) { + List values = rules.removeAll(name); + for (String value : values) { + try { + handler.handle(Integer.valueOf(value)); + } catch (NumberFormatException e) { + context.addWarning(8, name, value); + } + } + } + + private interface Handler { + void handle(T value); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/RecurrenceRuleScribe.java b/app/src/main/java/biweekly/io/scribe/property/RecurrenceRuleScribe.java new file mode 100644 index 0000000000..684d005ef2 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RecurrenceRuleScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.property.RecurrenceRule; +import biweekly.util.Recurrence; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RecurrenceRule} properties. + * @author Michael Angstadt + */ +public class RecurrenceRuleScribe extends RecurrencePropertyScribe { + public RecurrenceRuleScribe() { + super(RecurrenceRule.class, "RRULE"); + } + + @Override + protected RecurrenceRule newInstance(Recurrence recur) { + return new RecurrenceRule(recur); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/RefreshIntervalScribe.java b/app/src/main/java/biweekly/io/scribe/property/RefreshIntervalScribe.java new file mode 100644 index 0000000000..7a83f6d88d --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RefreshIntervalScribe.java @@ -0,0 +1,116 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.RefreshInterval; +import biweekly.util.Duration; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RefreshInterval} properties. + * @author Michael Angstadt + */ +public class RefreshIntervalScribe extends ICalPropertyScribe { + public RefreshIntervalScribe() { + super(RefreshInterval.class, "REFRESH-INTERVAL", ICalDataType.DURATION); + } + + @Override + protected String _writeText(RefreshInterval property, WriteContext context) { + Duration duration = property.getValue(); + if (duration != null) { + return duration.toString(); + } + + return ""; + } + + @Override + protected RefreshInterval _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value); + } + + @Override + protected void _writeXml(RefreshInterval property, XCalElement element, WriteContext context) { + String durationStr = null; + + Duration duration = property.getValue(); + if (duration != null) { + durationStr = duration.toString(); + } + + element.append(dataType(property, null), durationStr); + } + + @Override + protected RefreshInterval _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return parse(value); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(RefreshInterval property, WriteContext context) { + Duration value = property.getValue(); + if (value != null) { + return JCalValue.single(value.toString()); + } + + return JCalValue.single(""); + } + + @Override + protected RefreshInterval _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = value.asSingle(); + return parse(valueStr); + } + + private RefreshInterval parse(String value) { + if (value == null) { + return new RefreshInterval((Duration) null); + } + + try { + Duration duration = Duration.parse(value); + return new RefreshInterval(duration); + } catch (IllegalArgumentException e) { + throw new CannotParseException(18); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/RelatedToScribe.java b/app/src/main/java/biweekly/io/scribe/property/RelatedToScribe.java new file mode 100644 index 0000000000..6052331033 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RelatedToScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.RelatedTo; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RelatedTo} properties. + * @author Michael Angstadt + */ +public class RelatedToScribe extends TextPropertyScribe { + public RelatedToScribe() { + super(RelatedTo.class, "RELATED-TO"); + } + + @Override + protected RelatedTo newInstance(String value, ICalVersion version) { + return new RelatedTo(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/RepeatScribe.java b/app/src/main/java/biweekly/io/scribe/property/RepeatScribe.java new file mode 100644 index 0000000000..14948b39dc --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RepeatScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.Repeat; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Repeat} properties. + * @author Michael Angstadt + */ +public class RepeatScribe extends IntegerPropertyScribe { + public RepeatScribe() { + super(Repeat.class, "REPEAT"); + } + + @Override + protected Repeat newInstance(Integer value) { + return new Repeat(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/RequestStatusScribe.java b/app/src/main/java/biweekly/io/scribe/property/RequestStatusScribe.java new file mode 100644 index 0000000000..1526061588 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/RequestStatusScribe.java @@ -0,0 +1,123 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.RequestStatus; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueBuilder; +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.StructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link RequestStatus} properties. + * @author Michael Angstadt + */ +public class RequestStatusScribe extends ICalPropertyScribe { + public RequestStatusScribe() { + super(RequestStatus.class, "REQUEST-STATUS", ICalDataType.TEXT); + } + + @Override + protected String _writeText(RequestStatus property, WriteContext context) { + SemiStructuredValueBuilder builder = new SemiStructuredValueBuilder(); + builder.append(property.getStatusCode()); + builder.append(property.getDescription()); + builder.append(property.getExceptionText()); + boolean escapeCommas = (context.getVersion() != ICalVersion.V1_0); + return builder.build(escapeCommas, true); + } + + @Override + protected RequestStatus _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + SemiStructuredValueIterator it = new SemiStructuredValueIterator(value); + + RequestStatus requestStatus = new RequestStatus(it.next()); + requestStatus.setDescription(it.next()); + requestStatus.setExceptionText(it.next()); + return requestStatus; + } + + @Override + protected void _writeXml(RequestStatus property, XCalElement element, WriteContext context) { + String code = property.getStatusCode(); + element.append("code", code); + + String description = property.getDescription(); + element.append("description", description); + + String data = property.getExceptionText(); + if (data != null) { + element.append("data", data); + } + } + + @Override + protected RequestStatus _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + String code = element.first("code"); + if (code == null) { + throw missingXmlElements("code"); + } + + RequestStatus requestStatus = new RequestStatus(s(code)); + requestStatus.setDescription(s(element.first("description"))); //optional field + requestStatus.setExceptionText(s(element.first("data"))); //optional field + return requestStatus; + } + + @Override + protected JCalValue _writeJson(RequestStatus property, WriteContext context) { + return JCalValue.structured(property.getStatusCode(), property.getDescription(), property.getExceptionText()); + } + + @Override + protected RequestStatus _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + StructuredValueIterator it = new StructuredValueIterator(value.asStructured()); + + RequestStatus requestStatus = new RequestStatus(it.nextValue()); + requestStatus.setDescription(it.nextValue()); + requestStatus.setExceptionText(it.nextValue()); + return requestStatus; + } + + private static String s(String str) { + return (str == null || str.isEmpty()) ? null : str; + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/ResourcesScribe.java b/app/src/main/java/biweekly/io/scribe/property/ResourcesScribe.java new file mode 100644 index 0000000000..9ec0fbc3ca --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/ResourcesScribe.java @@ -0,0 +1,45 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.parameter.ICalParameters; +import biweekly.property.Resources; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Resources} properties. + * @author Michael Angstadt + */ +public class ResourcesScribe extends TextListPropertyScribe { + public ResourcesScribe() { + super(Resources.class, "RESOURCES"); + } + + @Override + public Resources newInstance(ICalDataType dataType, ICalParameters parameters) { + return new Resources(); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/SequenceScribe.java b/app/src/main/java/biweekly/io/scribe/property/SequenceScribe.java new file mode 100644 index 0000000000..6977f7d924 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/SequenceScribe.java @@ -0,0 +1,43 @@ +package biweekly.io.scribe.property; + +import biweekly.property.Sequence; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Sequence} properties. + * @author Michael Angstadt + */ +public class SequenceScribe extends IntegerPropertyScribe { + public SequenceScribe() { + super(Sequence.class, "SEQUENCE"); + } + + @Override + protected Sequence newInstance(Integer value) { + return new Sequence(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/SourceScribe.java b/app/src/main/java/biweekly/io/scribe/property/SourceScribe.java new file mode 100644 index 0000000000..d88cce74bf --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/SourceScribe.java @@ -0,0 +1,45 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.property.Source; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Source} properties. + * @author Michael Angstadt + */ +public class SourceScribe extends TextPropertyScribe { + public SourceScribe() { + super(Source.class, "SOURCE", ICalDataType.URI); + } + + @Override + protected Source newInstance(String value, ICalVersion version) { + return new Source(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/StatusScribe.java b/app/src/main/java/biweekly/io/scribe/property/StatusScribe.java new file mode 100644 index 0000000000..e9d738a546 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/StatusScribe.java @@ -0,0 +1,58 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.io.WriteContext; +import biweekly.property.Status; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Status} properties. + * @author Michael Angstadt + */ +public class StatusScribe extends TextPropertyScribe { + public StatusScribe() { + super(Status.class, "STATUS"); + } + + @Override + protected String _writeText(Status property, WriteContext context) { + if (context.getVersion() == ICalVersion.V1_0 && property.isNeedsAction()) { + //vCal doesn't have a hyphen in the value + return "NEEDS ACTION"; + } + return super._writeText(property, context); + } + + @Override + protected Status newInstance(String value, ICalVersion version) { + if (version == ICalVersion.V1_0 && "NEEDS ACTION".equalsIgnoreCase(value)) { + //vCal doesn't have a hyphen in the value + return Status.needsAction(); + } + return new Status(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/SummaryScribe.java b/app/src/main/java/biweekly/io/scribe/property/SummaryScribe.java new file mode 100644 index 0000000000..71eea603b4 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/SummaryScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Summary; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Summary} properties. + * @author Michael Angstadt + */ +public class SummaryScribe extends TextPropertyScribe { + public SummaryScribe() { + super(Summary.class, "SUMMARY"); + } + + @Override + protected Summary newInstance(String value, ICalVersion version) { + return new Summary(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TextListPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/TextListPropertyScribe.java new file mode 100644 index 0000000000..cdfe264d72 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TextListPropertyScribe.java @@ -0,0 +1,59 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.parameter.ICalParameters; +import biweekly.property.ListProperty; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that have a list of text values. + * @param the property class + * @author Michael Angstadt + */ +public abstract class TextListPropertyScribe> extends ListPropertyScribe { + public TextListPropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName); + } + + @Override + protected ICalDataType _defaultDataType(ICalVersion version) { + return ICalDataType.TEXT; + } + + @Override + protected String writeValue(T property, String value, WriteContext context) { + return value; + } + + @Override + protected String readValue(T property, String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return value; + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/TextPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/TextPropertyScribe.java new file mode 100644 index 0000000000..0822933eca --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TextPropertyScribe.java @@ -0,0 +1,96 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.TextProperty; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that have text values. + * @param the property class + * @author Michael Angstadt + */ +public abstract class TextPropertyScribe extends ICalPropertyScribe { + public TextPropertyScribe(Class clazz, String propertyName) { + this(clazz, propertyName, ICalDataType.TEXT); + } + + public TextPropertyScribe(Class clazz, String propertyName, ICalDataType dataType) { + super(clazz, propertyName, dataType); + } + + @Override + protected String _writeText(T property, WriteContext context) { + String value = property.getValue(); + if (value != null) { + return VObjectPropertyValues.escape(value); + } + + return ""; + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return newInstance(value, context.getVersion()); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + element.append(dataType(property, context.getVersion()), property.getValue()); + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return newInstance(value, context.getVersion()); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + return JCalValue.single(property.getValue()); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return newInstance(value.asSingle(), context.getVersion()); + } + + protected abstract T newInstance(String value, ICalVersion version); +} diff --git a/app/src/main/java/biweekly/io/scribe/property/TimezoneIdScribe.java b/app/src/main/java/biweekly/io/scribe/property/TimezoneIdScribe.java new file mode 100644 index 0000000000..e2bb876506 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TimezoneIdScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.TimezoneId; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link TimezoneId} properties. + * @author Michael Angstadt + */ +public class TimezoneIdScribe extends TextPropertyScribe { + public TimezoneIdScribe() { + super(TimezoneId.class, "TZID"); + } + + @Override + protected TimezoneId newInstance(String value, ICalVersion version) { + return new TimezoneId(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TimezoneNameScribe.java b/app/src/main/java/biweekly/io/scribe/property/TimezoneNameScribe.java new file mode 100644 index 0000000000..b439b01567 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TimezoneNameScribe.java @@ -0,0 +1,52 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.TimezoneName; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link TimezoneName} properties. + * @author Michael Angstadt + */ +public class TimezoneNameScribe extends TextPropertyScribe { + public TimezoneNameScribe() { + super(TimezoneName.class, "TZNAME"); + } + + @Override + protected TimezoneName newInstance(String value, ICalVersion version) { + return new TimezoneName(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TimezoneOffsetFromScribe.java b/app/src/main/java/biweekly/io/scribe/property/TimezoneOffsetFromScribe.java new file mode 100644 index 0000000000..0e8f54a543 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TimezoneOffsetFromScribe.java @@ -0,0 +1,53 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.TimezoneOffsetFrom; +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link TimezoneOffsetFrom} properties. + * @author Michael Angstadt + */ +public class TimezoneOffsetFromScribe extends UtcOffsetPropertyScribe { + public TimezoneOffsetFromScribe() { + super(TimezoneOffsetFrom.class, "TZOFFSETFROM"); + } + + @Override + protected TimezoneOffsetFrom newInstance(UtcOffset offset) { + return new TimezoneOffsetFrom(offset); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TimezoneOffsetToScribe.java b/app/src/main/java/biweekly/io/scribe/property/TimezoneOffsetToScribe.java new file mode 100644 index 0000000000..14e9646afb --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TimezoneOffsetToScribe.java @@ -0,0 +1,53 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.TimezoneOffsetTo; +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link TimezoneOffsetTo} properties. + * @author Michael Angstadt + */ +public class TimezoneOffsetToScribe extends UtcOffsetPropertyScribe { + public TimezoneOffsetToScribe() { + super(TimezoneOffsetTo.class, "TZOFFSETTO"); + } + + @Override + protected TimezoneOffsetTo newInstance(UtcOffset offset) { + return new TimezoneOffsetTo(offset); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TimezoneScribe.java b/app/src/main/java/biweekly/io/scribe/property/TimezoneScribe.java new file mode 100644 index 0000000000..111d46953c --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TimezoneScribe.java @@ -0,0 +1,53 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalVersion; +import biweekly.property.Timezone; +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Timezone} properties. + * @author Michael Angstadt + */ +public class TimezoneScribe extends UtcOffsetPropertyScribe { + public TimezoneScribe() { + super(Timezone.class, "TZ"); + } + + @Override + protected Timezone newInstance(UtcOffset offset) { + return new Timezone(offset); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V1_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/TimezoneUrlScribe.java b/app/src/main/java/biweekly/io/scribe/property/TimezoneUrlScribe.java new file mode 100644 index 0000000000..7b560bb315 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TimezoneUrlScribe.java @@ -0,0 +1,53 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.property.TimezoneUrl; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link TimezoneUrl} properties. + * @author Michael Angstadt + */ +public class TimezoneUrlScribe extends TextPropertyScribe { + public TimezoneUrlScribe() { + super(TimezoneUrl.class, "TZURL", ICalDataType.URI); + } + + @Override + protected TimezoneUrl newInstance(String value, ICalVersion version) { + return new TimezoneUrl(value); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TransparencyScribe.java b/app/src/main/java/biweekly/io/scribe/property/TransparencyScribe.java new file mode 100644 index 0000000000..e369ae4e24 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TransparencyScribe.java @@ -0,0 +1,74 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.io.WriteContext; +import biweekly.property.Transparency; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Transparency} properties. + * @author Michael Angstadt + */ +public class TransparencyScribe extends TextPropertyScribe { + public TransparencyScribe() { + super(Transparency.class, "TRANSP"); + } + + @Override + protected String _writeText(Transparency property, WriteContext context) { + if (context.getVersion() == ICalVersion.V1_0) { + if (property.isOpaque()) { + return "0"; + } + if (property.isTransparent()) { + return "1"; + } + } + + return super._writeText(property, context); + } + + @Override + protected Transparency newInstance(String value, ICalVersion version) { + if (version == ICalVersion.V1_0) { + try { + int intValue = Integer.parseInt(value); + switch (intValue) { + case 0: + return Transparency.opaque(); + case 1: + //values greater than "1" provide implementation-specific semantics + return Transparency.transparent(); + } + } catch (NumberFormatException e) { + //ignore + } + } + + return new Transparency(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/TriggerScribe.java b/app/src/main/java/biweekly/io/scribe/property/TriggerScribe.java new file mode 100644 index 0000000000..23d2990532 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/TriggerScribe.java @@ -0,0 +1,177 @@ +package biweekly.io.scribe.property; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.Trigger; +import biweekly.util.Duration; +import biweekly.util.ICalDate; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Trigger} properties. + * @author Michael Angstadt + */ +public class TriggerScribe extends ICalPropertyScribe { + public TriggerScribe() { + super(Trigger.class, "TRIGGER", ICalDataType.DURATION); + } + + @Override + protected ICalDataType _dataType(Trigger property, ICalVersion version) { + return (property.getDate() == null) ? ICalDataType.DURATION : ICalDataType.DATE_TIME; + } + + @Override + protected String _writeText(Trigger property, WriteContext context) { + Duration duration = property.getDuration(); + if (duration != null) { + return duration.toString(); + } + + Date date = property.getDate(); + return date(date, property, context).extended(false).write(); + } + + @Override + protected Trigger _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + + try { + ICalDate date = date(value).parse(); + Trigger property = new Trigger(date); + context.addDate(date, property, parameters); + return property; + } catch (IllegalArgumentException e) { + //unable to parse value as date, must be a duration + } + + try { + return new Trigger(Duration.parse(value), parameters.getRelated()); + } catch (IllegalArgumentException e) { + //unable to parse duration + } + + throw new CannotParseException(25); + } + + @Override + protected void _writeXml(Trigger property, XCalElement element, WriteContext context) { + Duration duration = property.getDuration(); + if (duration != null) { + element.append(ICalDataType.DURATION, duration.toString()); + return; + } + + Date date = property.getDate(); + if (date != null) { + String dateStr = date(date, property, context).extended(true).write(); + element.append(ICalDataType.DATE_TIME, dateStr); + return; + } + + element.append(defaultDataType(context.getVersion()), ""); + } + + @Override + protected Trigger _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + String value = element.first(ICalDataType.DURATION); + if (value != null) { + try { + return new Trigger(Duration.parse(value), parameters.getRelated()); + } catch (IllegalArgumentException e) { + throw new CannotParseException(26, value); + } + } + + value = element.first(ICalDataType.DATE_TIME); + if (value != null) { + try { + ICalDate date = date(value).parse(); + Trigger property = new Trigger(date); + context.addDate(date, property, parameters); + return property; + } catch (IllegalArgumentException e) { + throw new CannotParseException(27, value); + } + } + + throw missingXmlElements(ICalDataType.DURATION, ICalDataType.DATE_TIME); + } + + @Override + protected JCalValue _writeJson(Trigger property, WriteContext context) { + Duration duration = property.getDuration(); + if (duration != null) { + return JCalValue.single(duration.toString()); + } + + Date date = property.getDate(); + if (date != null) { + String dateStr = date(date, property, context).extended(true).write(); + return JCalValue.single(dateStr); + } + + return JCalValue.single(""); + } + + @Override + protected Trigger _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + String valueStr = value.asSingle(); + + try { + ICalDate date = date(valueStr).parse(); + Trigger property = new Trigger(date); + context.addDate(date, property, parameters); + return property; + } catch (IllegalArgumentException e) { + //must be a duration + } + + try { + return new Trigger(Duration.parse(valueStr), parameters.getRelated()); + } catch (IllegalArgumentException e) { + throw new CannotParseException(25); + } + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/UidScribe.java b/app/src/main/java/biweekly/io/scribe/property/UidScribe.java new file mode 100644 index 0000000000..b20e71c2ef --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/UidScribe.java @@ -0,0 +1,44 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalVersion; +import biweekly.property.Uid; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Uid} properties. + * @author Michael Angstadt + */ +public class UidScribe extends TextPropertyScribe { + public UidScribe() { + super(Uid.class, "UID"); + } + + @Override + protected Uid newInstance(String value, ICalVersion version) { + return new Uid(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/UrlScribe.java b/app/src/main/java/biweekly/io/scribe/property/UrlScribe.java new file mode 100644 index 0000000000..848ffbcfd9 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/UrlScribe.java @@ -0,0 +1,45 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.property.Url; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Url} properties. + * @author Michael Angstadt + */ +public class UrlScribe extends TextPropertyScribe { + public UrlScribe() { + super(Url.class, "URL", ICalDataType.URI); + } + + @Override + protected Url newInstance(String value, ICalVersion version) { + return new Url(value); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/UtcOffsetPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/UtcOffsetPropertyScribe.java new file mode 100644 index 0000000000..4b7086cd78 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/UtcOffsetPropertyScribe.java @@ -0,0 +1,117 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.UtcOffsetProperty; +import biweekly.util.UtcOffset; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals properties that have UTC offset values. + * @param the property class + * @author Michael Angstadt + */ +public abstract class UtcOffsetPropertyScribe extends ICalPropertyScribe { + public UtcOffsetPropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName, ICalDataType.UTC_OFFSET); + } + + @Override + protected String _writeText(T property, WriteContext context) { + UtcOffset offset = property.getValue(); + if (offset != null) { + return offset.toString(false); + } + + return ""; + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + return parse(value); + } + + @Override + protected void _writeXml(T property, XCalElement element, WriteContext context) { + String offsetStr = null; + + UtcOffset offset = property.getValue(); + if (offset != null) { + offsetStr = offset.toString(true); + } + + element.append(dataType(property, null), offsetStr); + } + + @Override + protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return parse(value); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(T property, WriteContext context) { + UtcOffset offset = property.getValue(); + if (offset != null) { + return JCalValue.single(offset.toString(true)); + } + + return JCalValue.single(""); + } + + @Override + protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(value.asSingle()); + } + + protected abstract T newInstance(UtcOffset offset); + + private T parse(String value) { + if (value == null) { + return newInstance(null); + } + + try { + return newInstance(UtcOffset.parse(value)); + } catch (IllegalArgumentException e) { + throw new CannotParseException(28); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/VCalAlarmPropertyScribe.java b/app/src/main/java/biweekly/io/scribe/property/VCalAlarmPropertyScribe.java new file mode 100644 index 0000000000..3efbf4b732 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/VCalAlarmPropertyScribe.java @@ -0,0 +1,190 @@ +package biweekly.io.scribe.property; + +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.component.VAlarm; +import biweekly.io.CannotParseException; +import biweekly.io.DataModelConversionException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.parameter.ICalParameters; +import biweekly.property.Action; +import biweekly.property.Trigger; +import biweekly.property.VCalAlarmProperty; +import biweekly.util.Duration; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link VCalAlarmProperty} properties. + * @author Michael Angstadt + */ +public abstract class VCalAlarmPropertyScribe extends ICalPropertyScribe { + public VCalAlarmPropertyScribe(Class clazz, String propertyName) { + super(clazz, propertyName); + } + + public VCalAlarmPropertyScribe(Class clazz, String propertyName, ICalDataType defaultDataType) { + super(clazz, propertyName, defaultDataType); + } + + @Override + protected String _writeText(T property, WriteContext context) { + List values = new ArrayList(); + + Date start = property.getStart(); + String value = date(start, property, context).extended(false).write(); + values.add(value); + + Duration snooze = property.getSnooze(); + value = (snooze == null) ? "" : snooze.toString(); + values.add(value); + + Integer repeat = property.getRepeat(); + value = (repeat == null) ? "" : repeat.toString(); + values.add(value); + + List dataValues = writeData(property); + values.addAll(dataValues); + + boolean escapeCommas = (context.getVersion() != ICalVersion.V1_0); + return VObjectPropertyValues.writeSemiStructured(values, escapeCommas, true); + } + + @Override + protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + SemiStructuredValueIterator it = new SemiStructuredValueIterator(value); + + String next = next(it); + Date start; + try { + start = (next == null) ? null : date(next).parse(); + } catch (IllegalArgumentException e) { + throw new CannotParseException(27, next); + } + + next = next(it); + Duration snooze; + try { + snooze = (next == null) ? null : Duration.parse(next); + } catch (IllegalArgumentException e) { + throw new CannotParseException(26, next); + } + + next = next(it); + Integer repeat; + try { + repeat = (next == null) ? null : Integer.valueOf(next); + } catch (IllegalArgumentException e) { + throw new CannotParseException(24, next); + } + + T property = create(dataType, it); + property.setStart(start); + property.setSnooze(snooze); + property.setRepeat(repeat); + property.setParameters(parameters); + + DataModelConversionException conversionException = new DataModelConversionException(property); + VAlarm valarm = toVAlarm(property); + conversionException.getComponents().add(valarm); + throw conversionException; + } + + private String next(SemiStructuredValueIterator it) { + String next = it.next(); + if (next == null) { + return null; + } + + next = next.trim(); + return next.isEmpty() ? null : next; + } + + /** + * Converts an instance of a vCal alarm property into a {@link VAlarm} + * component. + * @param property the property to convert + * @return the component + */ + protected VAlarm toVAlarm(T property) { + Trigger trigger = new Trigger(property.getStart()); + VAlarm valarm = new VAlarm(action(), trigger); + valarm.setDuration(property.getSnooze()); + valarm.setRepeat(property.getRepeat()); + + toVAlarm(valarm, property); + return valarm; + } + + /** + * Generates the part of the property value that will be included after the + * part of the value that is common to all vCal alarm properties. + * @param property the property + * @return the values + */ + protected abstract List writeData(T property); + + /** + * Creates a new instance of the property and populates it with the portion + * of data that is specific to this vCal alarm property. + * @param dataType the data type + * @param it an iterator to the property value that is positioned at the + * "value" portion of the property value (after the values that are common + * to all vCal alarm properties) + * @return the new property + */ + protected abstract T create(ICalDataType dataType, SemiStructuredValueIterator it); + + /** + * Determines what kind of {@link Action} property this vCal alarm property + * maps to, and returns a new instance of this property. + * @return a new {@link Action} property + */ + protected abstract Action action(); + + /** + * Populates a {@link VAlarm} component with data that is unique to this + * specific kind of vCal alarm property. + * @param valarm the component + * @param property the property + */ + protected abstract void toVAlarm(VAlarm valarm, T property); + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V1_0); + } +} diff --git a/app/src/main/java/biweekly/io/scribe/property/VersionScribe.java b/app/src/main/java/biweekly/io/scribe/property/VersionScribe.java new file mode 100644 index 0000000000..e1be661dd4 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/VersionScribe.java @@ -0,0 +1,118 @@ +package biweekly.io.scribe.property; + +import biweekly.ICalDataType; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.parameter.ICalParameters; +import biweekly.property.Version; +import biweekly.util.VersionNumber; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues.SemiStructuredValueIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Version} properties. + * @author Michael Angstadt + */ +public class VersionScribe extends ICalPropertyScribe { + public VersionScribe() { + super(Version.class, "VERSION", ICalDataType.TEXT); + } + + @Override + protected String _writeText(Version property, WriteContext context) { + StringBuilder sb = new StringBuilder(); + + if (property.getMinVersion() != null) { + sb.append(property.getMinVersion()).append(';'); + } + if (property.getMaxVersion() != null) { + sb.append(property.getMaxVersion()); + } + + return sb.toString(); + } + + @Override + protected Version _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + SemiStructuredValueIterator it = new SemiStructuredValueIterator(value); + String one = it.next(); + String two = it.next(); + + String min = null; + String max; + if (two == null) { + max = one; + } else { + min = one; + max = two; + } + + return parse(min, max); + } + + @Override + protected void _writeXml(Version property, XCalElement element, WriteContext context) { + VersionNumber max = property.getMaxVersion(); + String value = (max == null) ? null : max.toString(); + element.append(dataType(property, null), value); + } + + @Override + protected Version _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + ICalDataType dataType = defaultDataType(context.getVersion()); + String value = element.first(dataType); + if (value != null) { + return parse(null, value); + } + + throw missingXmlElements(dataType); + } + + @Override + protected JCalValue _writeJson(Version property, WriteContext context) { + VersionNumber max = property.getMaxVersion(); + String value = (max == null) ? null : max.toString(); + return JCalValue.single(value); + } + + @Override + protected Version _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + return parse(null, value.asSingle()); + } + + private Version parse(String min, String max) { + try { + return new Version(min, max); + } catch (IllegalArgumentException e) { + throw new CannotParseException(30); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/XmlScribe.java b/app/src/main/java/biweekly/io/scribe/property/XmlScribe.java new file mode 100644 index 0000000000..8780aac419 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/XmlScribe.java @@ -0,0 +1,139 @@ +package biweekly.io.scribe.property; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.xml.transform.OutputKeys; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.WriteContext; +import biweekly.io.json.JCalValue; +import biweekly.io.xml.XCalElement; +import biweekly.io.xml.XCalNamespaceContext; +import biweekly.parameter.ICalParameters; +import biweekly.property.Xml; +import biweekly.util.XmlUtils; + +import com.github.mangstadt.vinnie.io.VObjectPropertyValues; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Marshals {@link Xml} properties. + * @author Michael Angstadt + */ +public class XmlScribe extends ICalPropertyScribe { + //TODO on writing to plain text: convert to base64 if the string contains values that are illegal within a plain text value (p.17) + public XmlScribe() { + super(Xml.class, "XML", ICalDataType.TEXT); + } + + @Override + protected String _writeText(Xml property, WriteContext context) { + Document value = property.getValue(); + if (value != null) { + String xml = valueToString(value); + return VObjectPropertyValues.escape(xml); + } + + return ""; + } + + @Override + protected Xml _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + value = VObjectPropertyValues.unescape(value); + try { + return new Xml(value); + } catch (SAXException e) { + throw new CannotParseException(29); + } + } + + @Override + protected void _writeXml(Xml property, XCalElement element, WriteContext context) { + /* + * Note: Xml properties are handled as a special case when writing xCal + * documents, so this method should never get called. + */ + super._writeXml(property, element, context); + } + + @Override + protected Xml _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { + Xml xml = new Xml(element.getElement()); + + //remove the element + Element root = xml.getValue().getDocumentElement(); + for (Element child : XmlUtils.toElementList(root.getChildNodes())) { + if ("parameters".equals(child.getLocalName()) && XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) { + root.removeChild(child); + } + } + + return xml; + } + + @Override + protected JCalValue _writeJson(Xml property, WriteContext context) { + Document value = property.getValue(); + if (value != null) { + String xml = valueToString(value); + return JCalValue.single(xml); + } + + return JCalValue.single(""); + } + + @Override + protected Xml _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { + try { + String xml = value.asSingle(); + return new Xml(xml); + } catch (SAXException e) { + throw new CannotParseException(29); + } + } + + private String valueToString(Document document) { + Map props = new HashMap(); + props.put(OutputKeys.OMIT_XML_DECLARATION, "yes"); + return XmlUtils.toString(document, props); + } + + @Override + public Set getSupportedVersions() { + return EnumSet.of(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/scribe/property/package-info.java b/app/src/main/java/biweekly/io/scribe/property/package-info.java new file mode 100644 index 0000000000..96b06cda15 --- /dev/null +++ b/app/src/main/java/biweekly/io/scribe/property/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes that marshal and unmarshal properties in various formats. + */ +package biweekly.io.scribe.property; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/text/ICalReader.java b/app/src/main/java/biweekly/io/text/ICalReader.java new file mode 100644 index 0000000000..de642667d9 --- /dev/null +++ b/app/src/main/java/biweekly/io/text/ICalReader.java @@ -0,0 +1,487 @@ +package biweekly.io.text; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.CannotParseException; +import biweekly.io.DataModelConversionException; +import biweekly.io.ParseWarning; +import biweekly.io.SkipMeException; +import biweekly.io.StreamReader; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.io.scribe.property.RawPropertyScribe; +import biweekly.parameter.Encoding; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.util.Utf8Reader; + +import com.github.mangstadt.vinnie.VObjectProperty; +import com.github.mangstadt.vinnie.io.Context; +import com.github.mangstadt.vinnie.io.SyntaxRules; +import com.github.mangstadt.vinnie.io.VObjectDataListener; +import com.github.mangstadt.vinnie.io.VObjectReader; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Parses {@link ICalendar} objects from a plain-text iCalendar data stream. + *

+ *

+ * Example: + *

+ * + *
+ * File file = new File("icals.ics");
+ * ICalReader reader = null;
+ * try {
+ *   reader = new ICalReader(file);
+ *   ICalendar ical;
+ *   while ((ical = reader.readNext()) != null) {
+ *     //...
+ *   }
+ * } finally {
+ *   if (reader != null) reader.close();
+ * }
+ * 
+ * @author Michael Angstadt + * @see 1.0 specs + * @see RFC 2445 + * @see RFC 5545 + */ +public class ICalReader extends StreamReader { + private static final String VCALENDAR_COMPONENT_NAME = ScribeIndex.getICalendarScribe().getComponentName(); //"VCALENDAR" + + private final VObjectReader reader; + private final ICalVersion defaultVersion; + + /** + * Creates a new iCalendar reader. + * @param str the string to read from + */ + public ICalReader(String str) { + this(str, ICalVersion.V2_0); + } + + /** + * Creates a new iCalendar reader. + * @param str the string to read from + * @param defaultVersion the version to assume the iCalendar object is in + * until a VERSION property is encountered (defaults to 2.0) + */ + public ICalReader(String str, ICalVersion defaultVersion) { + this(new StringReader(str), defaultVersion); + } + + /** + * Creates a new iCalendar reader. + * @param in the input stream to read from + */ + public ICalReader(InputStream in) { + this(in, ICalVersion.V2_0); + } + + /** + * Creates a new iCalendar reader. + * @param defaultVersion the version to assume the iCalendar object is in + * until a VERSION property is encountered (defaults to 2.0) + * @param in the input stream to read from + */ + public ICalReader(InputStream in, ICalVersion defaultVersion) { + this(new Utf8Reader(in), defaultVersion); + } + + /** + * Creates a new iCalendar reader. + * @param file the file to read from + * @throws FileNotFoundException if the file doesn't exist + */ + public ICalReader(File file) throws FileNotFoundException { + this(file, ICalVersion.V2_0); + } + + /** + * Creates a new iCalendar reader. + * @param file the file to read from + * @param defaultVersion the version to assume the iCalendar object is in + * until a VERSION property is encountered (defaults to 2.0) + * @throws FileNotFoundException if the file doesn't exist + */ + public ICalReader(File file, ICalVersion defaultVersion) throws FileNotFoundException { + this(new BufferedReader(new Utf8Reader(file)), defaultVersion); + } + + /** + * Creates a new iCalendar reader. + * @param reader the reader to read from + */ + public ICalReader(Reader reader) { + this(reader, ICalVersion.V2_0); + } + + /** + * Creates a new iCalendar reader. + * @param reader the reader to read from + * @param defaultVersion the version to assume the iCalendar object is in + * until a VERSION property is encountered (defaults to 2.0) + */ + public ICalReader(Reader reader, ICalVersion defaultVersion) { + SyntaxRules rules = SyntaxRules.iCalendar(); + rules.setDefaultSyntaxStyle(defaultVersion.getSyntaxStyle()); + this.reader = new VObjectReader(reader, rules); + this.defaultVersion = defaultVersion; + } + + /** + * Gets whether the reader will decode parameter values that use circumflex + * accent encoding (enabled by default). This escaping mechanism allows + * newlines and double quotes to be included in parameter values. + * @return true if circumflex accent decoding is enabled, false if not + * @see VObjectReader#isCaretDecodingEnabled() + */ + public boolean isCaretDecodingEnabled() { + return reader.isCaretDecodingEnabled(); + } + + /** + * Sets whether the reader will decode parameter values that use circumflex + * accent encoding (enabled by default). This escaping mechanism allows + * newlines and double quotes to be included in parameter values. This only + * applies to version 2.0 iCalendar objects. + * @param enable true to use circumflex accent decoding, false not to + * @see VObjectReader#setCaretDecodingEnabled(boolean) + */ + public void setCaretDecodingEnabled(boolean enable) { + reader.setCaretDecodingEnabled(enable); + } + + /** + *

+ * Gets the character set to use when decoding quoted-printable values if + * the property has no CHARSET parameter, or if the CHARSET parameter is not + * a valid character set. + *

+ *

+ * By default, the Reader's character encoding will be used. If the Reader + * has no character encoding, then the system's default character encoding + * will be used. + *

+ * @return the character set + */ + public Charset getDefaultQuotedPrintableCharset() { + return reader.getDefaultQuotedPrintableCharset(); + } + + /** + *

+ * Sets the character set to use when decoding quoted-printable values if + * the property has no CHARSET parameter, or if the CHARSET parameter is not + * a valid character set. + *

+ *

+ * By default, the Reader's character encoding will be used. If the Reader + * has no character encoding, then the system's default character encoding + * will be used. + *

+ * @param charset the character set + */ + public void setDefaultQuotedPrintableCharset(Charset charset) { + reader.setDefaultQuotedPrintableCharset(charset); + } + + /** + *

+ * Gets the iCalendar version that this reader will assume each iCalendar + * object is formatted in up until a VERSION property is encountered. + *

+ *

+ * All standards-compliant iCalendar objects contain a VERSION property at + * the very beginning of the object, so for the vast majority of iCalendar + * objects, this setting does nothing. This setting is needed for when the + * iCalendar object does not have a VERSION property or for when the VERSION + * property is not located at the beginning of the object. + *

+ * @return the default version (defaults to "2.0") + */ + public ICalVersion getDefaultVersion() { + return defaultVersion; + } + + @Override + protected ICalendar _readNext() throws IOException { + VObjectDataListenerImpl listener = new VObjectDataListenerImpl(); + reader.parse(listener); + return listener.ical; + } + + private class VObjectDataListenerImpl implements VObjectDataListener { + private ICalendar ical = null; + private ICalVersion version = defaultVersion; + private ComponentStack stack = new ComponentStack(); + + public void onComponentBegin(String name, Context vobjectContext) { + //ignore everything until a VCALENDAR component is read + if (ical == null && !isVCalendarComponent(name)) { + return; + } + + ICalComponent parentComponent = stack.peek(); + + ICalComponentScribe scribe = index.getComponentScribe(name, version); + ICalComponent component = scribe.emptyInstance(); + stack.push(component); + + if (parentComponent == null) { + ical = (ICalendar) component; + context.setVersion(version); + } else { + parentComponent.addComponent(component); + } + } + + public void onComponentEnd(String name, Context vobjectContext) { + //VCALENDAR component not read yet, ignore + if (ical == null) { + return; + } + + /* + * VObjectDataListener guarantees correct ordering of component + * begin/end callback invocations (see javadocs), so we can pop + * blindly without checking if the component name matches. + */ + stack.pop(); + + //stop reading when "END:VCALENDAR" is reached + if (stack.isEmpty()) { + vobjectContext.stop(); + } + } + + public void onProperty(VObjectProperty vobjectProperty, Context vobjectContext) { + //VCALENDAR component not read yet, ignore + if (ical == null) { + return; + } + + String propertyName = vobjectProperty.getName(); + ICalParameters parameters = new ICalParameters(vobjectProperty.getParameters().getMap()); + String value = vobjectProperty.getValue(); + + context.getWarnings().clear(); + context.setLineNumber(vobjectContext.getLineNumber()); + context.setPropertyName(propertyName); + + ICalPropertyScribe scribe = index.getPropertyScribe(propertyName, version); + + //process nameless parameters + processNamelessParameters(parameters, version); + + //get the data type (VALUE parameter) + ICalDataType dataType = parameters.getValue(); + parameters.setValue(null); + if (dataType == null) { + //use the property's default data type if there is no VALUE parameter + dataType = scribe.defaultDataType(version); + } + + ICalComponent parentComponent = stack.peek(); + try { + ICalProperty property = scribe.parseText(value, dataType, parameters, context); + parentComponent.addProperty(property); + } catch (SkipMeException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(0, e.getMessage()) + .build() + ); + //@formatter:on + } catch (CannotParseException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(e) + .build() + ); + //@formatter:on + ICalProperty property = new RawPropertyScribe(propertyName).parseText(value, dataType, parameters, context); + parentComponent.addProperty(property); + } catch (DataModelConversionException e) { + for (ICalProperty property : e.getProperties()) { + parentComponent.addProperty(property); + } + for (ICalComponent component : e.getComponents()) { + parentComponent.addComponent(component); + } + } + + warnings.addAll(context.getWarnings()); + } + + public void onVersion(String value, Context vobjectContext) { + //ignore if we are not directly under the root VCALENDAR component + if (stack.size() != 1) { + return; + } + + version = ICalVersion.get(value); + context.setVersion(version); + } + + public void onWarning(com.github.mangstadt.vinnie.io.Warning warning, VObjectProperty property, Exception thrown, Context vobjectContext) { + //VCALENDAR component not read yet, ignore + if (ical == null) { + return; + } + + //@formatter:off + warnings.add(new ParseWarning.Builder() + .lineNumber(vobjectContext.getLineNumber()) + .propertyName((property == null) ? null : property.getName()) + .message(warning.getMessage()) + .build() + ); + //@formatter:on + } + + private boolean isVCalendarComponent(String componentName) { + return VCALENDAR_COMPONENT_NAME.equals(componentName); + } + + /** + * Assigns names to all nameless parameters. v2.0 requires all + * parameters to have names, but v1.0 does not. + * @param parameters the parameters + * @param version the iCal version + */ + private void processNamelessParameters(ICalParameters parameters, ICalVersion version) { + List namelessParamValues = parameters.removeAll(null); + if (namelessParamValues.isEmpty()) { + return; + } + + if (version != ICalVersion.V1_0) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(4, namelessParamValues) + .build() + ); + //@formatter:on + } + + for (String paramValue : namelessParamValues) { + String paramName = guessParameterName(paramValue); + parameters.put(paramName, paramValue); + } + } + + /** + * Makes a guess as to what a parameter value's name should be. + * @param value the parameter value + * @return the guessed name + */ + private String guessParameterName(String value) { + if (ICalDataType.find(value) != null) { + return ICalParameters.VALUE; + } + + if (Encoding.find(value) != null) { + return ICalParameters.ENCODING; + } + + //otherwise, assume it's a TYPE + return ICalParameters.TYPE; + } + } + + /** + * Keeps track of the hierarchy of nested components. + */ + private static class ComponentStack { + private final List components = new ArrayList(); + + /** + * Gets the top component from the stack. + * @return the component or null if the stack is empty + */ + public ICalComponent peek() { + return isEmpty() ? null : components.get(components.size() - 1); + } + + /** + * Adds a component to the stack + * @param component the component + */ + public void push(ICalComponent component) { + components.add(component); + } + + /** + * Removes the top component from the stack and returns it. + * @return the top component or null if the stack is empty + */ + public ICalComponent pop() { + return isEmpty() ? null : components.remove(components.size() - 1); + } + + /** + * Determines if the stack is empty. + * @return true if it's empty, false if not + */ + public boolean isEmpty() { + return components.isEmpty(); + } + + /** + * Gets the size of the stack. + * @return the size + */ + public int size() { + return components.size(); + } + } + + /** + * Closes the input stream. + * @throws IOException if there's a problem closing the input stream + */ + public void close() throws IOException { + reader.close(); + } +} diff --git a/app/src/main/java/biweekly/io/text/ICalWriter.java b/app/src/main/java/biweekly/io/text/ICalWriter.java new file mode 100644 index 0000000000..ea075a690c --- /dev/null +++ b/app/src/main/java/biweekly/io/text/ICalWriter.java @@ -0,0 +1,345 @@ +package biweekly.io.text; + +import static biweekly.io.DataModelConverter.convert; + +import java.io.File; +import java.io.FileWriter; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.List; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.component.VTimezone; +import biweekly.io.DataModelConversionException; +import biweekly.io.DataModelConverter.VCalTimezoneProperties; +import biweekly.io.SkipMeException; +import biweekly.io.StreamWriter; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.parameter.ICalParameters; +import biweekly.property.Daylight; +import biweekly.property.ICalProperty; +import biweekly.property.Timezone; +import biweekly.property.Version; +import biweekly.util.Utf8Writer; + +import com.github.mangstadt.vinnie.VObjectParameters; +import com.github.mangstadt.vinnie.io.VObjectWriter; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Writes {@link ICalendar} objects to a plain-text iCalendar data stream. + *

+ *

+ * Example: + *

+ * + *
+ * ICalendar ical1 = ...
+ * ICalendar ical2 = ...
+ * File file = new File("icals.ics");
+ * ICalWriter writer = null;
+ * try {
+ *   writer = new ICalWriter(file, ICalVersion.V2_0);
+ *   writer.write(ical1);
+ *   writer.write(ical2);
+ * } finally {
+ *   if (writer != null) writer.close();
+ * }
+ * 
+ * + *

+ * Changing the line folding settings: + *

+ * + *
+ * ICalWriter writer = new ICalWriter(...);
+ * 
+ * //disable line folding
+ * writer.getVObjectWriter().getFoldedLineWriter().setLineLength(null);
+ * 
+ * //set line length (defaults to 75)
+ * writer.getVObjectWriter().getFoldedLineWriter().setLineLength(50);
+ * 
+ * //change folded line indent string (defaults to one space character)
+ * writer.getVObjectWriter().getFoldedLineWriter().setIndent("\t");
+ *
+ * 
+ * @author Michael Angstadt + * @see 1.0 specs + * @see RFC 2445 + * @see RFC 5545 + */ +public class ICalWriter extends StreamWriter implements Flushable { + private final VObjectWriter writer; + private ICalVersion targetVersion; + + /** + * Creates a new iCalendar writer. + * @param out the output stream to write to + * @param targetVersion the iCalendar version to adhere to + */ + public ICalWriter(OutputStream out, ICalVersion targetVersion) { + this((targetVersion == ICalVersion.V1_0) ? new OutputStreamWriter(out) : new Utf8Writer(out), targetVersion); + } + + /** + * Creates a new iCalendar writer. + * @param file the file to write to + * @param targetVersion the iCalendar version to adhere to + * @throws IOException if the file cannot be written to + */ + public ICalWriter(File file, ICalVersion targetVersion) throws IOException { + this(file, false, targetVersion); + } + + /** + * Creates a new iCalendar writer. + * @param file the file to write to + * @param targetVersion the iCalendar version to adhere to + * @param append true to append to the end of the file, false to overwrite + * it + * @throws IOException if the file cannot be written to + */ + public ICalWriter(File file, boolean append, ICalVersion targetVersion) throws IOException { + this((targetVersion == ICalVersion.V1_0) ? new FileWriter(file, append) : new Utf8Writer(file, append), targetVersion); + } + + /** + * Creates a new iCalendar writer. + * @param writer the writer to write to + * @param targetVersion the iCalendar version to adhere to + */ + public ICalWriter(Writer writer, ICalVersion targetVersion) { + this.writer = new VObjectWriter(writer, targetVersion.getSyntaxStyle()); + this.targetVersion = targetVersion; + } + + /** + * Gets the writer object that is used internally to write to the output + * stream. + * @return the raw writer + */ + public VObjectWriter getVObjectWriter() { + return writer; + } + + /** + * Gets the version that the written iCalendar objects will adhere to. + * @return the iCalendar version + */ + @Override + public ICalVersion getTargetVersion() { + return targetVersion; + } + + /** + * Sets the version that the written iCalendar objects will adhere to. + * @param targetVersion the iCalendar version + */ + public void setTargetVersion(ICalVersion targetVersion) { + this.targetVersion = targetVersion; + writer.setSyntaxStyle(targetVersion.getSyntaxStyle()); + } + + /** + *

+ * Gets whether the writer will apply circumflex accent encoding on + * parameter values (disabled by default). This escaping mechanism allows + * for newlines and double quotes to be included in parameter values. It is + * only supported by iCalendar version 2.0. + *

+ * @return true if circumflex accent encoding is enabled, false if not + * @see VObjectWriter#isCaretEncodingEnabled() + */ + public boolean isCaretEncodingEnabled() { + return writer.isCaretEncodingEnabled(); + } + + /** + *

+ * Sets whether the writer will apply circumflex accent encoding on + * parameter values (disabled by default). This escaping mechanism allows + * for newlines and double quotes to be included in parameter values. It is + * only supported by iCalendar version 2.0. + *

+ *

+ * Note that this encoding mechanism is defined separately from the + * iCalendar specification and may not be supported by the consumer of the + * iCalendar object. + *

+ * @param enable true to use circumflex accent encoding, false not to + * @see VObjectWriter#setCaretEncodingEnabled(boolean) + */ + public void setCaretEncodingEnabled(boolean enable) { + writer.setCaretEncodingEnabled(enable); + } + + @Override + protected void _write(ICalendar ical) throws IOException { + writeComponent(ical, null); + } + + /** + * Writes a component to the data stream. + * @param component the component to write + * @param parent the parent component + * @throws IOException if there's a problem writing to the data stream + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void writeComponent(ICalComponent component, ICalComponent parent) throws IOException { + boolean inICalendar = component instanceof ICalendar; + boolean inVCalRoot = inICalendar && getTargetVersion() == ICalVersion.V1_0; + boolean inICalRoot = inICalendar && getTargetVersion() != ICalVersion.V1_0; + + ICalComponentScribe componentScribe = index.getComponentScribe(component); + try { + componentScribe.checkForDataModelConversions(component, parent, getTargetVersion()); + } catch (DataModelConversionException e) { + for (ICalComponent c : e.getComponents()) { + writeComponent(c, parent); + } + for (ICalProperty p : e.getProperties()) { + writeProperty(p); + } + return; + } + + writer.writeBeginComponent(componentScribe.getComponentName()); + + List propertyObjs = componentScribe.getProperties(component); + if (inICalendar && component.getProperty(Version.class) == null) { + propertyObjs.add(0, new Version(getTargetVersion())); + } + + for (Object propertyObj : propertyObjs) { + context.setParent(component); //set parent here incase a scribe resets the parent + ICalProperty property = (ICalProperty) propertyObj; + writeProperty(property); + } + + List subComponents = componentScribe.getComponents(component); + if (inICalRoot) { + //add the VTIMEZONE components + Collection timezones = getTimezoneComponents(); + for (VTimezone timezone : timezones) { + if (!subComponents.contains(timezone)) { + subComponents.add(0, timezone); + } + } + } + + for (Object subComponentObj : subComponents) { + ICalComponent subComponent = (ICalComponent) subComponentObj; + writeComponent(subComponent, component); + } + + if (inVCalRoot) { + Collection timezones = getTimezoneComponents(); + if (!timezones.isEmpty()) { + VTimezone timezone = timezones.iterator().next(); + VCalTimezoneProperties props = convert(timezone, context.getDates()); + + Timezone tz = props.getTz(); + if (tz != null) { + writeProperty(tz); + } + for (Daylight daylight : props.getDaylights()) { + writeProperty(daylight); + } + } + } + + writer.writeEndComponent(componentScribe.getComponentName()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void writeProperty(ICalProperty property) throws IOException { + ICalPropertyScribe scribe = index.getPropertyScribe(property); + + //marshal property + String value; + try { + value = scribe.writeText(property, context); + } catch (SkipMeException e) { + return; + } catch (DataModelConversionException e) { + for (ICalComponent c : e.getComponents()) { + writeComponent(c, context.getParent()); + } + for (ICalProperty p : e.getProperties()) { + writeProperty(p); + } + return; + } + + //get parameters + ICalParameters parameters = scribe.prepareParameters(property, context); + + /* + * Set the property's data type. + * + * Only add a VALUE parameter if the data type is: (1) not "unknown" (2) + * different from the property's default data type + */ + ICalDataType dataType = scribe.dataType(property, targetVersion); + if (dataType != null && dataType != scribe.defaultDataType(targetVersion)) { + parameters = new ICalParameters(parameters); + parameters.setValue(dataType); + } + + //get the property name + String propertyName = scribe.getPropertyName(getTargetVersion()); + + //write property to data stream + writer.writeProperty(null, propertyName, new VObjectParameters(parameters.getMap()), value); + } + + /** + * Flushes the output stream. + * @throws IOException if there's a problem flushing the output stream + */ + public void flush() throws IOException { + writer.flush(); + } + + /** + * Closes the output stream. + * @throws IOException if there's a problem closing the output stream + */ + public void close() throws IOException { + writer.close(); + } +} diff --git a/app/src/main/java/biweekly/io/text/package-info.java b/app/src/main/java/biweekly/io/text/package-info.java new file mode 100644 index 0000000000..725dcfdb36 --- /dev/null +++ b/app/src/main/java/biweekly/io/text/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for reading and writing traditional, plain-text iCalendar objects. + */ +package biweekly.io.text; \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/xml/XCalDocument.java b/app/src/main/java/biweekly/io/xml/XCalDocument.java new file mode 100644 index 0000000000..5c24754bc7 --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalDocument.java @@ -0,0 +1,813 @@ +package biweekly.io.xml; + +import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; +import static biweekly.io.xml.XCalQNames.COMPONENTS; +import static biweekly.io.xml.XCalQNames.ICALENDAR; +import static biweekly.io.xml.XCalQNames.PARAMETERS; +import static biweekly.io.xml.XCalQNames.PROPERTIES; +import static biweekly.io.xml.XCalQNames.VCALENDAR; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.component.VTimezone; +import biweekly.io.CannotParseException; +import biweekly.io.ParseWarning; +import biweekly.io.SkipMeException; +import biweekly.io.StreamReader; +import biweekly.io.StreamWriter; +import biweekly.io.scribe.ScribeIndex; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.component.ICalendarScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.property.Version; +import biweekly.property.Xml; +import biweekly.util.Utf8Writer; +import biweekly.util.XmlUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +//@formatter:off +/** + *

+ * Represents an XML document that contains iCalendar objects ("xCal" standard). + * This class can be used to read and write xCal documents. + *

+ *

+ * Examples: + *

+ * + *
+ * String xml =
+ * "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ * "<icalendar xmlns=\"urn:ietf:params:xml:ns:icalendar-2.0\">" +
+ *   "<vcalendar>" +
+ *     "<properties>" +
+ *       "<prodid><text>-//Example Inc.//Example Client//EN</text></prodid>" +
+ *       "<version><text>2.0</text></version>" +
+ *     "</properties>" +
+ *     "<components>" +
+ *       "<vevent>" +
+ *         "<properties>" +
+ *           "<dtstart><date-time>2013-06-27T13:00:00Z</date-time></dtstart>" +
+ *           "<dtend><date-time>2013-06-27T15:00:00Z</date-time></dtend>" +
+ *           "<summary><text>Team Meeting</text></summary>" +
+ *         "</properties>" +
+ *       "</vevent>" +
+ *     "</components>" +
+ *   "</vcalendar>" +
+ * "</icalendar>";
+ *     
+ * //parsing an existing xCal document
+ * XCalDocument xcal = new XCalDocument(xml);
+ * List<ICalendar> icals = xcal.getICalendars();
+ * 
+ * //creating an empty xCal document
+ * XCalDocument xcal = new XCalDocument();
+ * 
+ * //ICalendar objects can be added at any time
+ * ICalendar ical = new ICalendar();
+ * xcal.addICalendar(ical);
+ * 
+ * //retrieving the raw XML DOM
+ * Document document = xcal.getDocument();
+ * 
+ * //call one of the "write()" methods to output the xCal document
+ * File file = new File("meeting.xml");
+ * xcal.write(file);
+ * 
+ * @author Michael Angstadt + * @see RFC 6321 + */ +//@formatter:on +public class XCalDocument { + private static final ICalendarScribe icalMarshaller = ScribeIndex.getICalendarScribe(); + private static final XCalNamespaceContext nsContext = new XCalNamespaceContext("xcal"); + + private final Document document; + private Element icalendarRootElement; + + /** + * Parses an xCal document from a string. + * @param xml the xCal document in the form of a string + * @throws SAXException if there's a problem parsing the XML + */ + public XCalDocument(String xml) throws SAXException { + this(XmlUtils.toDocument(xml)); + } + + /** + * Parses an xCal document from an input stream. + * @param in the input stream to read the the xCal document from + * @throws IOException if there's a problem reading from the input stream + * @throws SAXException if there's a problem parsing the XML + */ + public XCalDocument(InputStream in) throws SAXException, IOException { + this(XmlUtils.toDocument(in)); + } + + /** + * Parses an xCal document from a file. + * @param file the file containing the xCal document + * @throws IOException if there's a problem reading from the file + * @throws SAXException if there's a problem parsing the XML + */ + public XCalDocument(File file) throws SAXException, IOException { + this(XmlUtils.toDocument(file)); + } + + /** + *

+ * Parses an xCal document from a reader. + *

+ *

+ * Note that use of this constructor is discouraged. It ignores the + * character encoding that is defined within the XML document itself, and + * should only be used if the encoding is undefined or if the encoding needs + * to be ignored for whatever reason. The {@link #XCalDocument(InputStream)} + * constructor should be used instead, since it takes the XML document's + * character encoding into account when parsing. + *

+ * @param reader the reader to read the xCal document from + * @throws IOException if there's a problem reading from the reader + * @throws SAXException if there's a problem parsing the XML + */ + public XCalDocument(Reader reader) throws SAXException, IOException { + this(XmlUtils.toDocument(reader)); + } + + /** + * Wraps an existing XML DOM object. + * @param document the XML DOM that contains the xCal document + */ + public XCalDocument(Document document) { + this.document = document; + + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setNamespaceContext(nsContext); + + try { + //find the element + String prefix = nsContext.getPrefix(); + icalendarRootElement = (Element) xpath.evaluate("//" + prefix + ":" + ICALENDAR.getLocalPart(), document, XPathConstants.NODE); + } catch (XPathExpressionException e) { + //never thrown, xpath expression is hard coded + } + } + + /** + * Creates an empty xCal document. + */ + public XCalDocument() { + document = XmlUtils.createDocument(); + icalendarRootElement = document.createElementNS(ICALENDAR.getNamespaceURI(), ICALENDAR.getLocalPart()); + document.appendChild(icalendarRootElement); + } + + /** + * Gets the raw XML DOM object. + * @return the XML DOM + */ + public Document getDocument() { + return document; + } + + /** + * Parses all iCalendar objects from this XML document. + * @return the parsed iCalendar objects + */ + public List getICalendars() { + try { + return reader().readAll(); + } catch (IOException e) { + //not thrown because reading from DOM + throw new RuntimeException(e); + } + } + + /** + * Adds an iCalendar object to this XML document. + * @param ical the iCalendar object to add + */ + public void addICalendar(ICalendar ical) { + writer().write(ical); + } + + /** + * Creates a {@link StreamReader} object that parses iCalendar objects from + * this XML document. + * @return the reader + */ + public StreamReader reader() { + return new XCalDocumentStreamReader(); + } + + /** + * Creates a {@link StreamWriter} object that adds iCalendar objects to this + * XML document. + * @return the writer + */ + public XCalDocumentStreamWriter writer() { + return new XCalDocumentStreamWriter(); + } + + /** + * Writes the xCal document to a string. + * @return the XML string + */ + public String write() { + return write((Integer) null); + } + + /** + * Writes the xCal document to a string. + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @return the XML string + */ + public String write(Integer indent) { + return write(indent, null); + } + + /** + * Writes the xCal document to a string. + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + * @return the XML string + */ + public String write(Integer indent, String xmlVersion) { + return write(new XCalOutputProperties(indent, xmlVersion)); + } + + /** + * Writes the xCal document to a string. + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperty}) + * @return the XML string + */ + public String write(Map outputProperties) { + StringWriter sw = new StringWriter(); + try { + write(sw, outputProperties); + } catch (TransformerException e) { + //shouldn't be thrown because we're writing to a string + throw new RuntimeException(e); + } + return sw.toString(); + } + + /** + * Writes the xCal document to an output stream. + * @param out the output stream to write to (UTF-8 encoding will be used) + * @throws TransformerException if there's a problem writing to the output + * stream + */ + public void write(OutputStream out) throws TransformerException { + write(out, (Integer) null); + } + + /** + * Writes the xCal document to an output stream. + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @throws TransformerException if there's a problem writing to the output + * stream + */ + public void write(OutputStream out, Integer indent) throws TransformerException { + write(out, indent, null); + } + + /** + * Writes the xCal document to an output stream. + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + * @throws TransformerException if there's a problem writing to the output + * stream + */ + public void write(OutputStream out, Integer indent, String xmlVersion) throws TransformerException { + write(out, new XCalOutputProperties(indent, xmlVersion)); + } + + /** + * Writes the xCal document to an output stream. + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperty}) + * @throws TransformerException if there's a problem writing to the output + * stream + */ + public void write(OutputStream out, Map outputProperties) throws TransformerException { + write(new Utf8Writer(out), outputProperties); + } + + /** + * Writes the xCal document to a file. + * @param file the file to write to (UTF-8 encoding will be used) + * @throws IOException if there's a problem writing to the file + * @throws TransformerException if there's a problem writing the XML + */ + public void write(File file) throws TransformerException, IOException { + write(file, (Integer) null); + } + + /** + * Writes the xCal document to a file. + * @param file the file to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @throws IOException if there's a problem writing to the file + * @throws TransformerException if there's a problem writing the XML + */ + public void write(File file, Integer indent) throws TransformerException, IOException { + write(file, indent, null); + } + + /** + * Writes the xCal document to a file. + * @param file the file to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + * @throws IOException if there's a problem writing to the file + * @throws TransformerException if there's a problem writing the XML + */ + public void write(File file, Integer indent, String xmlVersion) throws TransformerException, IOException { + write(file, new XCalOutputProperties(indent, xmlVersion)); + } + + /** + * Writes the xCal document to a file. + * @param file the file to write to (UTF-8 encoding will be used) + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperty}) + * @throws IOException if there's a problem writing to the file + * @throws TransformerException if there's a problem writing the XML + */ + public void write(File file, Map outputProperties) throws TransformerException, IOException { + Writer writer = new Utf8Writer(file); + try { + write(writer, outputProperties); + } finally { + writer.close(); + } + } + + /** + * Writes the xCal document to a writer. + * @param writer the writer + * @throws TransformerException if there's a problem writing to the writer + */ + public void write(Writer writer) throws TransformerException { + write(writer, (Integer) null); + } + + /** + * Writes the xCal document to a writer. + * @param writer the writer + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @throws TransformerException if there's a problem writing to the writer + */ + public void write(Writer writer, Integer indent) throws TransformerException { + write(writer, indent, null); + } + + /** + * Writes the xCal document to a writer. + * @param writer the writer + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + * @throws TransformerException if there's a problem writing to the writer + */ + public void write(Writer writer, Integer indent, String xmlVersion) throws TransformerException { + write(writer, new XCalOutputProperties(indent, xmlVersion)); + } + + /** + * Writes the xCal document to a writer. + * @param writer the writer + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperty}) + * @throws TransformerException if there's a problem writing to the writer + */ + public void write(Writer writer, Map outputProperties) throws TransformerException { + Transformer transformer; + try { + transformer = TransformerFactory.newInstance().newTransformer(); + } catch (TransformerConfigurationException e) { + //should never be thrown because we're not doing anything fancy with the configuration + throw new RuntimeException(e); + } catch (TransformerFactoryConfigurationError e) { + //should never be thrown because we're not doing anything fancy with the configuration + throw new RuntimeException(e); + } + + /* + * Using Transformer#setOutputProperties(Properties) doesn't work for + * some reason for setting the number of indentation spaces. + */ + for (Map.Entry entry : outputProperties.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + transformer.setOutputProperty(key, value); + } + + DOMSource source = new DOMSource(document); + StreamResult result = new StreamResult(writer); + transformer.transform(source, result); + } + + @Override + public String toString() { + return write(2); + } + + private class XCalDocumentStreamReader extends StreamReader { + private final Iterator vcalendarElements = getVCalendarElements().iterator(); + + @Override + protected ICalendar _readNext() throws IOException { + if (!vcalendarElements.hasNext()) { + return null; + } + + context.setVersion(ICalVersion.V2_0); + Element vcalendarElement = vcalendarElements.next(); + return parseICal(vcalendarElement); + } + + private ICalendar parseICal(Element icalElement) { + ICalComponent root = parseComponent(icalElement); + if (root instanceof ICalendar) { + return (ICalendar) root; + } + + //shouldn't happen, since only elements are passed into this method + ICalendar ical = icalMarshaller.emptyInstance(); + ical.addComponent(root); + return ical; + } + + private ICalComponent parseComponent(Element componentElement) { + //create the component object + ICalComponentScribe scribe = index.getComponentScribe(componentElement.getLocalName(), ICalVersion.V2_0); + ICalComponent component = scribe.emptyInstance(); + boolean isICalendar = component instanceof ICalendar; + + //parse properties + for (Element propertyWrapperElement : getChildElements(componentElement, PROPERTIES)) { //there should be only one element, but parse them all incase there are more + for (Element propertyElement : XmlUtils.toElementList(propertyWrapperElement.getChildNodes())) { + ICalProperty property = parseProperty(propertyElement); + if (property == null) { + continue; + } + + //set "ICalendar.version" if the value of the VERSION property is recognized + //otherwise, unmarshal VERSION like a normal property + if (isICalendar && property instanceof Version) { + Version version = (Version) property; + ICalVersion icalVersion = version.toICalVersion(); + if (icalVersion != null) { + context.setVersion(icalVersion); + continue; + } + } + + component.addProperty(property); + } + } + + //parse sub-components + for (Element componentWrapperElement : getChildElements(componentElement, COMPONENTS)) { //there should be only one element, but parse them all incase there are more + for (Element subComponentElement : XmlUtils.toElementList(componentWrapperElement.getChildNodes())) { + if (!XCAL_NS.equals(subComponentElement.getNamespaceURI())) { + continue; + } + + ICalComponent subComponent = parseComponent(subComponentElement); + component.addComponent(subComponent); + } + } + + return component; + } + + private ICalProperty parseProperty(Element propertyElement) { + ICalParameters parameters = parseParameters(propertyElement); + String propertyName = propertyElement.getLocalName(); + QName qname = new QName(propertyElement.getNamespaceURI(), propertyName); + + context.getWarnings().clear(); + context.setPropertyName(propertyName); + ICalPropertyScribe scribe = index.getPropertyScribe(qname); + try { + ICalProperty property = scribe.parseXml(propertyElement, parameters, context); + warnings.addAll(context.getWarnings()); + return property; + } catch (SkipMeException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(0, e.getMessage()) + .build() + ); + //@formatter:on + return null; + } catch (CannotParseException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(e) + .build() + ); + //@formatter:on + + scribe = index.getPropertyScribe(Xml.class); + return scribe.parseXml(propertyElement, parameters, context); + } + } + + private ICalParameters parseParameters(Element propertyElement) { + ICalParameters parameters = new ICalParameters(); + + for (Element parametersElement : getChildElements(propertyElement, PARAMETERS)) { //there should be only one element, but parse them all incase there are more + List paramElements = XmlUtils.toElementList(parametersElement.getChildNodes()); + for (Element paramElement : paramElements) { + if (!XCAL_NS.equals(paramElement.getNamespaceURI())) { + continue; + } + + String name = paramElement.getLocalName().toUpperCase(); + List valueElements = XmlUtils.toElementList(paramElement.getChildNodes()); + if (valueElements.isEmpty()) { + //this should never be true if the xCal follows the specs + String value = paramElement.getTextContent(); + parameters.put(name, value); + continue; + } + + for (Element valueElement : valueElements) { + if (!XCAL_NS.equals(valueElement.getNamespaceURI())) { + continue; + } + + String value = valueElement.getTextContent(); + parameters.put(name, value); + } + } + } + + return parameters; + } + + private List getVCalendarElements() { + return (icalendarRootElement == null) ? Collections. emptyList() : getChildElements(icalendarRootElement, VCALENDAR); + } + + private List getChildElements(Element parent, QName qname) { + List elements = new ArrayList(); + for (Element child : XmlUtils.toElementList(parent.getChildNodes())) { + QName childQName = new QName(child.getNamespaceURI(), child.getLocalName()); + if (qname.equals(childQName)) { + elements.add(child); + } + } + return elements; + } + + public void close() { + //do nothing + } + } + + public class XCalDocumentStreamWriter extends XCalWriterBase { + @Override + public void write(ICalendar ical) { + try { + super.write(ical); + } catch (IOException e) { + //won't be thrown because we're writing to DOM + } + } + + @Override + protected void _write(ICalendar ical) { + Element element = buildComponentElement(ical); + + if (icalendarRootElement == null) { + icalendarRootElement = buildElement(ICALENDAR); + Element documentRoot = document.getDocumentElement(); + if (documentRoot == null) { + document.appendChild(icalendarRootElement); + } else { + documentRoot.appendChild(icalendarRootElement); + } + } + icalendarRootElement.appendChild(element); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Element buildComponentElement(ICalComponent component) { + ICalComponentScribe componentScribe = index.getComponentScribe(component); + Element componentElement = buildElement(componentScribe.getComponentName().toLowerCase()); + + Element propertiesWrapperElement = buildElement(PROPERTIES); + List propertyObjs = componentScribe.getProperties(component); + if (component instanceof ICalendar && component.getProperty(Version.class) == null) { + //add a version property + propertyObjs.add(0, new Version(targetVersion)); + } + + for (Object propertyObj : propertyObjs) { + context.setParent(component); //set parent here incase a scribe resets the parent + ICalProperty property = (ICalProperty) propertyObj; + + //create property element + Element propertyElement = buildPropertyElement(property); + if (propertyElement != null) { + propertiesWrapperElement.appendChild(propertyElement); + } + } + if (propertiesWrapperElement.hasChildNodes()) { + componentElement.appendChild(propertiesWrapperElement); + } + + List subComponents = componentScribe.getComponents(component); + if (component instanceof ICalendar) { + //add the VTIMEZONE components that were auto-generated by TimezoneOptions + Collection tzs = getTimezoneComponents(); + for (VTimezone tz : tzs) { + if (!subComponents.contains(tz)) { + subComponents.add(0, tz); + } + } + } + Element componentsWrapperElement = buildElement(COMPONENTS); + for (Object subComponentObj : subComponents) { + ICalComponent subComponent = (ICalComponent) subComponentObj; + Element subComponentElement = buildComponentElement(subComponent); + if (subComponentElement != null) { + componentsWrapperElement.appendChild(subComponentElement); + } + } + if (componentsWrapperElement.hasChildNodes()) { + componentElement.appendChild(componentsWrapperElement); + } + + return componentElement; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Element buildPropertyElement(ICalProperty property) { + Element propertyElement; + ICalPropertyScribe scribe = index.getPropertyScribe(property); + + if (property instanceof Xml) { + Xml xml = (Xml) property; + + Document value = xml.getValue(); + if (value == null) { + return null; + } + + //import the XML element into the xCal DOM + propertyElement = value.getDocumentElement(); + propertyElement = (Element) document.importNode(propertyElement, true); + } else { + propertyElement = buildElement(scribe.getQName()); + + //marshal value + try { + scribe.writeXml(property, propertyElement, context); + } catch (SkipMeException e) { + return null; + } + } + + //build parameters + ICalParameters parameters = scribe.prepareParameters(property, context); + if (!parameters.isEmpty()) { + Element parametersElement = buildParametersElement(parameters); + propertyElement.insertBefore(parametersElement, propertyElement.getFirstChild()); + } + + return propertyElement; + } + + private Element buildParametersElement(ICalParameters parameters) { + Element parametersWrapperElement = buildElement(PARAMETERS); + + for (Map.Entry> parameter : parameters) { + String name = parameter.getKey().toLowerCase(); + ICalDataType dataType = parameterDataTypes.get(name); + String dataTypeStr = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); + + Element parameterElement = buildAndAppendElement(name, parametersWrapperElement); + for (String parameterValue : parameter.getValue()) { + Element parameterValueElement = buildAndAppendElement(dataTypeStr, parameterElement); + parameterValueElement.setTextContent(parameterValue); + } + } + + return parametersWrapperElement; + } + + private Element buildElement(String localName) { + return buildElement(new QName(XCAL_NS, localName)); + } + + private Element buildElement(QName qname) { + return document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); + } + + private Element buildAndAppendElement(String localName, Element parent) { + return buildAndAppendElement(new QName(XCAL_NS, localName), parent); + } + + private Element buildAndAppendElement(QName qname, Element parent) { + Element child = document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); + parent.appendChild(child); + return child; + } + + public void close() { + //do nothing + } + } +} diff --git a/app/src/main/java/biweekly/io/xml/XCalElement.java b/app/src/main/java/biweekly/io/xml/XCalElement.java new file mode 100644 index 0000000000..156424a1ac --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalElement.java @@ -0,0 +1,292 @@ +package biweekly.io.xml; + +import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import biweekly.ICalDataType; +import biweekly.util.XmlUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Wraps xCal functionality around an XML {@link Element} object. + * @author Michael Angstadt + */ +public class XCalElement { + private final Element element; + private final Document document; + + /** + * Creates a new xCal element. + * @param element the XML element to wrap + */ + public XCalElement(Element element) { + this.element = element; + document = element.getOwnerDocument(); + } + + /** + * Gets the first value of the given data type. + * @param dataType the data type to look for or null for the "unknown" data + * type + * @return the value or null if not found + */ + public String first(ICalDataType dataType) { + String dataTypeStr = toLocalName(dataType); + return first(dataTypeStr); + } + + /** + * Gets the value of the first child element with the given name. + * @param localName the name of the element + * @return the element's text or null if not found + */ + public String first(String localName) { + for (Element child : children()) { + if (localName.equals(child.getLocalName()) && XCAL_NS.equals(child.getNamespaceURI())) { + return child.getTextContent(); + } + } + return null; + } + + /** + * Gets all the values of a given data type. + * @param dataType the data type to look for or null for the "unknown" data + * type + * @return the values + */ + public List all(ICalDataType dataType) { + String dataTypeStr = toLocalName(dataType); + return all(dataTypeStr); + } + + /** + * Gets the values of all child elements that have the given name. + * @param localName the element name + * @return the values of the child elements + */ + public List all(String localName) { + List childrenText = new ArrayList(); + for (Element child : children()) { + if (localName.equals(child.getLocalName()) && XCAL_NS.equals(child.getNamespaceURI())) { + String text = child.getTextContent(); + childrenText.add(text); + } + } + return childrenText; + } + + /** + * Adds a value. + * @param dataType the data type or null for the "unknown" data type + * @param value the value + * @return the created element + */ + public Element append(ICalDataType dataType, String value) { + String dataTypeStr = toLocalName(dataType); + return append(dataTypeStr, value); + } + + /** + * Adds a child element. + * @param name the name of the child element + * @param value the value of the child element. + * @return the created element + */ + public Element append(String name, String value) { + Element child = document.createElementNS(XCAL_NS, name); + child.setTextContent(value); + element.appendChild(child); + return child; + } + + /** + * Adds a child element. + * @param name the name of the child element + * @return the created element + */ + public XCalElement append(String name) { + return new XCalElement(append(name, (String) null)); + } + + /** + * Adds an empty value. + * @param dataType the data type + * @return the created element + */ + public XCalElement append(ICalDataType dataType) { + return append(dataType.getName().toLowerCase()); + } + + /** + * Adds multiple child elements, each with the same name. + * @param name the name for all the child elements + * @param values the values of each child element + * @return the created elements + */ + public List append(String name, Collection values) { + List elements = new ArrayList(values.size()); + for (String value : values) { + elements.add(append(name, value)); + } + return elements; + } + + /** + * Gets the owner document. + * @return the owner document + */ + public Document document() { + return document; + } + + /** + * Gets the wrapped XML element. + * @return the wrapped XML element + */ + public Element getElement() { + return element; + } + + /** + * Gets the child elements of the wrapped XML element. + * @return the child elements + */ + private List children() { + return XmlUtils.toElementList(element.getChildNodes()); + } + + /** + * Gets all child elements with the given data type. + * @param dataType the data type + * @return the child elements + */ + public List children(ICalDataType dataType) { + String localName = dataType.getName().toLowerCase(); + List children = new ArrayList(); + for (Element child : children()) { + if (localName.equals(child.getLocalName()) && XCAL_NS.equals(child.getNamespaceURI())) { + children.add(new XCalElement(child)); + } + } + return children; + } + + /** + * Gets the first child element with the given data type. + * @param dataType the data type + * @return the child element or null if not found + */ + public XCalElement child(ICalDataType dataType) { + String localName = dataType.getName().toLowerCase(); + for (Element child : children()) { + if (localName.equals(child.getLocalName()) && XCAL_NS.equals(child.getNamespaceURI())) { + return new XCalElement(child); + } + } + return null; + } + + /** + * Finds the first child element that has the xCard namespace and returns + * its data type and value. If no such element is found, the parent + * {@link XCalElement}'s text content, along with a null data type, is + * returned. + * @return the value and data type + */ + public XCalValue firstValue() { + for (Element child : children()) { + String childNamespace = child.getNamespaceURI(); + if (XCAL_NS.equals(childNamespace)) { + ICalDataType dataType = toDataType(child.getLocalName()); + String value = child.getTextContent(); + return new XCalValue(dataType, value); + } + } + + return new XCalValue(null, element.getTextContent()); + } + + /** + * Gets the appropriate XML local name of a {@link ICalDataType} object. + * @param dataType the data type or null for "unknown" + * @return the local name (e.g. "text") + */ + private String toLocalName(ICalDataType dataType) { + return (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); + } + + /** + * Converts an XML local name to the appropriate {@link ICalDataType} + * object. + * @param localName the local name (e.g. "text") + * @return the data type or null for "unknown" + */ + private static ICalDataType toDataType(String localName) { + return "unknown".equals(localName) ? null : ICalDataType.get(localName); + } + + /** + * Represents the data type and value of a child element under an + * {@link XCalElement}. + */ + public static class XCalValue { + private final ICalDataType dataType; + private final String value; + + /** + * @param dataType the data type or null if "unknown" + * @param value the value + */ + public XCalValue(ICalDataType dataType, String value) { + this.dataType = dataType; + this.value = value; + } + + /** + * Gets the data type + * @return the data type or null if "unknown" + */ + public ICalDataType getDataType() { + return dataType; + } + + /** + * Get the value. + * @return the value + */ + public String getValue() { + return value; + } + } +} diff --git a/app/src/main/java/biweekly/io/xml/XCalNamespaceContext.java b/app/src/main/java/biweekly/io/xml/XCalNamespaceContext.java new file mode 100644 index 0000000000..10322ae596 --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalNamespaceContext.java @@ -0,0 +1,83 @@ +package biweekly.io.xml; + +import java.util.Collections; +import java.util.Iterator; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.xpath.XPath; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Used for xCal xpath expressions. + * @see XPath#setNamespaceContext(NamespaceContext) + * @author Michael Angstadt + */ +public class XCalNamespaceContext implements NamespaceContext { + /** + * The XML namespace for xCal documents. + */ + public static final String XCAL_NS = "urn:ietf:params:xml:ns:icalendar-2.0"; + + private final String prefix; + + /** + * Creates a new namespace context. + * @param prefix the prefix to use in xpath expressions + */ + public XCalNamespaceContext(String prefix) { + this.prefix = prefix; + } + + /** + * Gets the prefix to use in xpath expressions. + * @return the xpath prefix + */ + public String getPrefix() { + return prefix; + } + + //@Override + public String getNamespaceURI(String prefix) { + if (this.prefix.equals(prefix)) { + return XCAL_NS; + } + return null; + } + + //@Override + public String getPrefix(String ns) { + if (XCAL_NS.equals(ns)) { + return prefix; + } + return null; + } + + //@Override + public Iterator getPrefixes(String ns) { + return XCAL_NS.equals(ns) ? Collections.singletonList(prefix).iterator() : null; + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/io/xml/XCalOutputProperties.java b/app/src/main/java/biweekly/io/xml/XCalOutputProperties.java new file mode 100644 index 0000000000..d9e88360e7 --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalOutputProperties.java @@ -0,0 +1,126 @@ +package biweekly.io.xml; + +import java.util.HashMap; + +import javax.xml.transform.OutputKeys; + +import biweekly.Messages; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Helper class for setting commonly-used JAXP output properties. + * @author Michael Angstadt + */ +public class XCalOutputProperties extends HashMap { + private static final long serialVersionUID = -1038397031136827278L; + private static final String INDENT_AMT = "{http://xml.apache.org/xslt}indent-amount"; + + public XCalOutputProperties() { + put(OutputKeys.METHOD, "xml"); + } + + /** + * @param indent the number of indent spaces to use for pretty-printing or + * null to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + * @throws IllegalArgumentException if the indent amount is less than zero + */ + public XCalOutputProperties(Integer indent, String xmlVersion) { + this(); + setIndent(indent); + setXmlVersion(xmlVersion); + } + + /** + * Gets the number of indent spaces to use for pretty-printing. + * @return the number of indent spaces or null if pretty-printing is + * disabled + */ + public Integer getIndent() { + if (!"yes".equals(get(OutputKeys.INDENT))) { + return null; + } + + String value = get(INDENT_AMT); + return (value == null) ? null : Integer.valueOf(value); + } + + /** + * Sets the number of indent spaces to use for pretty-printing (disabled by + * default). + * @param indent the number of indent spaces to use or null to disable + * pretty-printing + * @throws IllegalArgumentException if the indent amount is less than zero + */ + public void setIndent(Integer indent) { + if (indent == null) { + remove(OutputKeys.INDENT); + remove(INDENT_AMT); + return; + } + + if (indent < 0) { + throw Messages.INSTANCE.getIllegalArgumentException(11); + } + + put(OutputKeys.INDENT, "yes"); + put(INDENT_AMT, indent.toString()); + } + + /** + * Gets the XML version to use. + * @return the XML version or null if not set + */ + public String getXmlVersion() { + return get(OutputKeys.VERSION); + } + + /** + *

+ * Sets the XML version to use (defaults to "1.0"). + *

+ *

+ * Note: Many JDKs only support 1.0 natively. For XML 1.1 support, add a + * JAXP library like xalan to your project. + *

+ * @param version the XML version or null to remove + */ + public void setXmlVersion(String version) { + if (version == null) { + remove(OutputKeys.VERSION); + return; + } + + put(OutputKeys.VERSION, version); + } +} diff --git a/app/src/main/java/biweekly/io/xml/XCalQNames.java b/app/src/main/java/biweekly/io/xml/XCalQNames.java new file mode 100644 index 0000000000..ffbb4a65fa --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalQNames.java @@ -0,0 +1,42 @@ +package biweekly.io.xml; + +import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; + +import javax.xml.namespace.QName; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Contains the XML element names of some of the standard xCard elements. + * @author Michael Angstadt + */ +public interface XCalQNames { + QName ICALENDAR = new QName(XCAL_NS, "icalendar"); + QName VCALENDAR = new QName(XCAL_NS, "vcalendar"); + QName COMPONENTS = new QName(XCAL_NS, "components"); + QName PROPERTIES = new QName(XCAL_NS, "properties"); + QName PARAMETERS = new QName(XCAL_NS, "parameters"); +} diff --git a/app/src/main/java/biweekly/io/xml/XCalReader.java b/app/src/main/java/biweekly/io/xml/XCalReader.java new file mode 100644 index 0000000000..4817d7847b --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalReader.java @@ -0,0 +1,620 @@ +package biweekly.io.xml; + +import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; +import static biweekly.io.xml.XCalQNames.COMPONENTS; +import static biweekly.io.xml.XCalQNames.ICALENDAR; +import static biweekly.io.xml.XCalQNames.PARAMETERS; +import static biweekly.io.xml.XCalQNames.PROPERTIES; +import static biweekly.io.xml.XCalQNames.VCALENDAR; + +import java.io.BufferedInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import javax.xml.namespace.QName; +import javax.xml.transform.ErrorListener; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stream.StreamSource; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.io.CannotParseException; +import biweekly.io.ParseContext; +import biweekly.io.ParseWarning; +import biweekly.io.SkipMeException; +import biweekly.io.StreamReader; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.property.Version; +import biweekly.property.Xml; +import biweekly.util.XmlUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. + */ + +/** + *

+ * Reads xCals (XML-encoded iCalendar objects) in a streaming fashion. + *

+ *

+ * Example: + *

+ * + *
+ * File file = new File("icals.xml");
+ * XCalReader reader = null;
+ * try {
+ *   reader = new XCalReader(file);
+ *   ICalendar ical;
+ *   while ((ical = reader.readNext()) != null) {
+ *     //...
+ *   }
+ * } finally {
+ *   if (reader != null) reader.close();
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 6321 + */ +public class XCalReader extends StreamReader { + private final Source source; + private final Closeable stream; + + private volatile ICalendar readICal; + private volatile TransformerException thrown; + + private final ReadThread thread = new ReadThread(); + private final Object lock = new Object(); + private final BlockingQueue readerBlock = new ArrayBlockingQueue(1); + private final BlockingQueue threadBlock = new ArrayBlockingQueue(1); + + /** + * @param str the string to read from + */ + public XCalReader(String str) { + this(new StringReader(str)); + } + + /** + * @param in the input stream to read from + */ + public XCalReader(InputStream in) { + source = new StreamSource(in); + stream = in; + } + + /** + * @param file the file to read from + * @throws FileNotFoundException if the file doesn't exist + */ + public XCalReader(File file) throws FileNotFoundException { + this(new BufferedInputStream(new FileInputStream(file))); + } + + /** + * @param reader the reader to read from + */ + public XCalReader(Reader reader) { + source = new StreamSource(reader); + stream = reader; + } + + /** + * @param node the DOM node to read from + */ + public XCalReader(Node node) { + source = new DOMSource(node); + stream = null; + } + + @Override + protected ICalendar _readNext() throws IOException { + readICal = null; + warnings.clear(); + context = new ParseContext(); + thrown = null; + + if (!thread.started) { + thread.start(); + } else { + if (thread.finished || thread.closed) { + return null; + } + + try { + threadBlock.put(lock); + } catch (InterruptedException e) { + return null; + } + } + + //wait until thread reads xCard + try { + readerBlock.take(); + } catch (InterruptedException e) { + return null; + } + + if (thrown != null) { + throw new IOException(thrown); + } + + return readICal; + } + + private class ReadThread extends Thread { + private final SAXResult result; + private final Transformer transformer; + private volatile boolean finished = false, started = false, closed = false; + + public ReadThread() { + setName(getClass().getSimpleName()); + + //create the transformer + try { + TransformerFactory factory = TransformerFactory.newInstance(); + XmlUtils.applyXXEProtection(factory); + + transformer = factory.newTransformer(); + } catch (TransformerConfigurationException e) { + //shouldn't be thrown because it's a simple configuration + throw new RuntimeException(e); + } + + //prevent error messages from being printed to stderr + transformer.setErrorListener(new NoOpErrorListener()); + + result = new SAXResult(new ContentHandlerImpl()); + } + + @Override + public void run() { + started = true; + + try { + transformer.transform(source, result); + } catch (TransformerException e) { + if (!thread.closed) { + thrown = e; + } + } finally { + finished = true; + try { + readerBlock.put(lock); + } catch (InterruptedException e) { + //ignore + } + } + } + } + + private class ContentHandlerImpl extends DefaultHandler { + private final Document DOC = XmlUtils.createDocument(); + private final XCalStructure structure = new XCalStructure(); + private final StringBuilder characterBuffer = new StringBuilder(); + private final LinkedList componentStack = new LinkedList(); + + private Element propertyElement, parent; + private QName paramName; + private ICalComponent curComponent; + private ICalParameters parameters; + + @Override + public void characters(char[] buffer, int start, int length) throws SAXException { + characterBuffer.append(buffer, start, length); + } + + @Override + public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException { + QName qname = new QName(namespace, localName); + String textContent = emptyCharacterBuffer(); + + if (structure.isEmpty()) { + // + if (ICALENDAR.equals(qname)) { + structure.push(ElementType.icalendar); + } + return; + } + + ElementType parentType = structure.peek(); + ElementType typeToPush = null; + if (parentType != null) { + switch (parentType) { + + case icalendar: + // + if (VCALENDAR.equals(qname)) { + ICalComponentScribe scribe = index.getComponentScribe(localName, ICalVersion.V2_0); + ICalComponent component = scribe.emptyInstance(); + + curComponent = component; + readICal = (ICalendar) component; + typeToPush = ElementType.component; + } + break; + + case component: + if (PROPERTIES.equals(qname)) { + // + typeToPush = ElementType.properties; + } else if (COMPONENTS.equals(qname)) { + // + componentStack.add(curComponent); + curComponent = null; + + typeToPush = ElementType.components; + } + break; + + case components: + //start component element + if (XCAL_NS.equals(namespace)) { + ICalComponentScribe scribe = index.getComponentScribe(localName, ICalVersion.V2_0); + curComponent = scribe.emptyInstance(); + + ICalComponent parent = componentStack.getLast(); + parent.addComponent(curComponent); + + typeToPush = ElementType.component; + } + break; + + case properties: + //start property element + propertyElement = createElement(namespace, localName, attributes); + parameters = new ICalParameters(); + parent = propertyElement; + typeToPush = ElementType.property; + break; + + case property: + // + if (PARAMETERS.equals(qname)) { + typeToPush = ElementType.parameters; + } + break; + + case parameters: + //inside of + if (XCAL_NS.equals(namespace)) { + paramName = qname; + typeToPush = ElementType.parameter; + } + break; + + case parameter: + //inside of a parameter element + if (XCAL_NS.equals(namespace)) { + typeToPush = ElementType.parameterValue; + } + break; + case parameterValue: + //should never have child elements + break; + } + } + + //append element to property element + if (propertyElement != null && typeToPush != ElementType.property && typeToPush != ElementType.parameters && !structure.isUnderParameters()) { + if (textContent.length() > 0) { + parent.appendChild(DOC.createTextNode(textContent)); + } + + Element element = createElement(namespace, localName, attributes); + parent.appendChild(element); + parent = element; + } + + structure.push(typeToPush); + } + + @Override + public void endElement(String namespace, String localName, String qName) throws SAXException { + String textContent = emptyCharacterBuffer(); + + if (structure.isEmpty()) { + //no elements were read yet + return; + } + + ElementType type = structure.pop(); + if (type == null && (propertyElement == null || structure.isUnderParameters())) { + //it's a non-xCal element + return; + } + + if (type != null) { + switch (type) { + case parameterValue: + parameters.put(paramName.getLocalPart(), textContent); + break; + + case parameter: + //do nothing + break; + + case parameters: + //do nothing + break; + + case property: + context.getWarnings().clear(); + context.setPropertyName(localName); + + propertyElement.appendChild(DOC.createTextNode(textContent)); + + //unmarshal property and add to parent component + QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName()); + ICalPropertyScribe scribe = index.getPropertyScribe(propertyQName); + try { + ICalProperty property = scribe.parseXml(propertyElement, parameters, context); + if (property instanceof Version && curComponent instanceof ICalendar) { + Version versionProp = (Version) property; + ICalVersion version = versionProp.toICalVersion(); + if (version != null) { + ICalendar ical = (ICalendar) curComponent; + ical.setVersion(version); + context.setVersion(version); + + propertyElement = null; + break; + } + } + + curComponent.addProperty(property); + warnings.addAll(context.getWarnings()); + } catch (SkipMeException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(0, e.getMessage()) + .build() + ); + //@formatter:on + } catch (CannotParseException e) { + //@formatter:off + warnings.add(new ParseWarning.Builder(context) + .message(e) + .build() + ); + //@formatter:on + + scribe = index.getPropertyScribe(Xml.class); + ICalProperty property = scribe.parseXml(propertyElement, parameters, context); + curComponent.addProperty(property); + } + + propertyElement = null; + break; + + case component: + curComponent = null; + + // + if (VCALENDAR.getNamespaceURI().equals(namespace) && VCALENDAR.getLocalPart().equals(localName)) { + //wait for readNext() to be called again + try { + readerBlock.put(lock); + threadBlock.take(); + } catch (InterruptedException e) { + throw new SAXException(e); + } + return; + } + break; + + case properties: + break; + + case components: + curComponent = componentStack.removeLast(); + break; + + case icalendar: + break; + } + } + + //append element to property element + if (propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters()) { + if (textContent.length() > 0) { + parent.appendChild(DOC.createTextNode(textContent)); + } + parent = (Element) parent.getParentNode(); + } + } + + private String emptyCharacterBuffer() { + String textContent = characterBuffer.toString(); + characterBuffer.setLength(0); + return textContent; + } + + private Element createElement(String namespace, String localName, Attributes attributes) { + Element element = DOC.createElementNS(namespace, localName); + applyAttributesTo(element, attributes); + return element; + } + + private void applyAttributesTo(Element element, Attributes attributes) { + for (int i = 0; i < attributes.getLength(); i++) { + String qname = attributes.getQName(i); + if (qname.startsWith("xmlns:")) { + continue; + } + + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + element.setAttribute(name, value); + } + } + } + + private enum ElementType { + /* + * Note: A value is missing for "vcalendar" because it is treated as a + * "component". + * + * Note: These enum values are in lower-case to make them stand out from + * the "XCalQNames" variable names, many of which are identically named. + */ + icalendar, components, properties, component, property, parameters, parameter, parameterValue; + } + + /** + *

+ * Keeps track of the structure of an xCal XML document. + *

+ * + *

+ * Note that this class is here because you can't just do QName comparisons + * on a one-by-one basis. The location of an XML element within the XML + * document is important too. It's possible for two elements to have the + * same QName, but be treated differently depending on their location (e.g. + * the {@code } property has a {@code } data type) + *

+ */ + private static class XCalStructure { + private final List stack = new ArrayList(); + + /** + * Pops the top element type off the stack. + * @return the element type or null if the stack is empty + */ + public ElementType pop() { + return isEmpty() ? null : stack.remove(stack.size() - 1); + } + + /** + * Looks at the top element type. + * @return the top element type or null if the stack is empty + */ + public ElementType peek() { + return isEmpty() ? null : stack.get(stack.size() - 1); + } + + /** + * Adds an element type to the stack. + * @param type the type to add or null if the XML element is not an xCal + * element + */ + public void push(ElementType type) { + stack.add(type); + } + + /** + * Determines if the leaf node is under a {@code } element. + * @return true if it is, false if not + */ + public boolean isUnderParameters() { + //get the first non-null type + ElementType nonNull = null; + for (int i = stack.size() - 1; i >= 0; i--) { + ElementType type = stack.get(i); + if (type != null) { + nonNull = type; + break; + } + } + + //@formatter:off + return + nonNull == ElementType.parameters || + nonNull == ElementType.parameter || + nonNull == ElementType.parameterValue; + //@formatter:on + } + + /** + * Determines if the stack is empty + * @return true if the stack is empty, false if not + */ + public boolean isEmpty() { + return stack.isEmpty(); + } + } + + /** + * An implementation of {@link ErrorListener} that doesn't do anything. + */ + private static class NoOpErrorListener implements ErrorListener { + public void error(TransformerException e) { + //do nothing + } + + public void fatalError(TransformerException e) { + //do nothing + } + + public void warning(TransformerException e) { + //do nothing + } + } + + /** + * Closes the underlying input stream. + */ + public void close() throws IOException { + if (thread.isAlive()) { + thread.closed = true; + thread.interrupt(); + } + + if (stream != null) { + stream.close(); + } + } +} diff --git a/app/src/main/java/biweekly/io/xml/XCalWriter.java b/app/src/main/java/biweekly/io/xml/XCalWriter.java new file mode 100644 index 0000000000..aef43c2e21 --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalWriter.java @@ -0,0 +1,539 @@ +package biweekly.io.xml; + +import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; +import static biweekly.io.xml.XCalQNames.COMPONENTS; +import static biweekly.io.xml.XCalQNames.ICALENDAR; +import static biweekly.io.xml.XCalQNames.PARAMETERS; +import static biweekly.io.xml.XCalQNames.PROPERTIES; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.xml.namespace.QName; +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import biweekly.ICalDataType; +import biweekly.ICalendar; +import biweekly.component.ICalComponent; +import biweekly.component.VTimezone; +import biweekly.io.SkipMeException; +import biweekly.io.scribe.component.ICalComponentScribe; +import biweekly.io.scribe.property.ICalPropertyScribe; +import biweekly.parameter.ICalParameters; +import biweekly.property.ICalProperty; +import biweekly.property.Version; +import biweekly.property.Xml; +import biweekly.util.Utf8Writer; +import biweekly.util.XmlUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. + */ + +/** + *

+ * Writes xCards (XML-encoded iCalendar objects) in a streaming fashion. + *

+ *

+ * Example: + *

+ * + *
+ * ICalendar ical1 = ...
+ * ICalendar ical2 = ...
+ * File file = new File("icals.xml");
+ * XCalWriter writer = null;
+ * try {
+ *   writer = new XCalWriter(file);
+ *   writer.write(ical1);
+ *   writer.write(ical2);
+ * } finally {
+ *   if (writer != null) writer.close();
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 6351 + */ +public class XCalWriter extends XCalWriterBase { + //How to use SAX to write XML: http://stackoverflow.com/q/4898590 + private final Document DOC = XmlUtils.createDocument(); + + private final Writer writer; + private final TransformerHandler handler; + private final boolean icalendarElementExists; + private boolean started = false; + + /** + * @param out the output stream to write to (UTF-8 encoding will be used) + */ + public XCalWriter(OutputStream out) { + this(out, (Integer) null); + } + + /** + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + */ + public XCalWriter(OutputStream out, Integer indent) { + this(out, indent, null); + } + + /** + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + */ + public XCalWriter(OutputStream out, Integer indent, String xmlVersion) { + this(new Utf8Writer(out), indent, xmlVersion); + } + + /** + * @param out the output stream to write to (UTF-8 encoding will be used) + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperties(Properties)}) + */ + public XCalWriter(OutputStream out, Map outputProperties) { + this(new Utf8Writer(out), outputProperties); + } + + /** + * @param file the file to write to (UTF-8 encoding will be used) + * @throws IOException if there's a problem opening the file + */ + public XCalWriter(File file) throws IOException { + this(file, (Integer) null); + } + + /** + * @param file the file to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @throws IOException if there's a problem opening the file + */ + public XCalWriter(File file, Integer indent) throws IOException { + this(file, indent, null); + } + + /** + * @param file the file to write to (UTF-8 encoding will be used) + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + * @throws IOException if there's a problem opening the file + */ + public XCalWriter(File file, Integer indent, String xmlVersion) throws IOException { + this(new Utf8Writer(file), indent, xmlVersion); + } + + /** + * @param file the file to write to (UTF-8 encoding will be used) + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperties(Properties)}) + * @throws IOException if there's a problem opening the file + */ + public XCalWriter(File file, Map outputProperties) throws IOException { + this(new Utf8Writer(file), outputProperties); + } + + /** + * @param writer the writer to write to + */ + public XCalWriter(Writer writer) { + this(writer, (Integer) null); + } + + /** + * @param writer the writer to write to + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + */ + public XCalWriter(Writer writer, Integer indent) { + this(writer, indent, null); + } + + /** + * @param writer the writer to write to + * @param indent the number of indent spaces to use for pretty-printing or + * "null" to disable pretty-printing (disabled by default) + * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many + * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library + * like xalan to your project) + */ + public XCalWriter(Writer writer, Integer indent, String xmlVersion) { + this(writer, new XCalOutputProperties(indent, xmlVersion)); + } + + /** + * @param writer the writer to write to + * @param outputProperties properties to assign to the JAXP transformer (see + * {@link Transformer#setOutputProperties(Properties)}) + */ + public XCalWriter(Writer writer, Map outputProperties) { + this(writer, null, outputProperties); + } + + /** + * @param parent the DOM node to add child elements to + */ + public XCalWriter(Node parent) { + this(null, parent, new HashMap()); + } + + private XCalWriter(Writer writer, Node parent, Map outputProperties) { + this.writer = writer; + + if (parent instanceof Document) { + Node root = parent.getFirstChild(); + if (root != null) { + parent = root; + } + } + this.icalendarElementExists = isICalendarElement(parent); + + try { + SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); + handler = factory.newTransformerHandler(); + } catch (TransformerConfigurationException e) { + throw new RuntimeException(e); + } + + Transformer transformer = handler.getTransformer(); + + /* + * Using Transformer#setOutputProperties(Properties) doesn't work for + * some reason for setting the number of indentation spaces. + */ + for (Map.Entry entry : outputProperties.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + transformer.setOutputProperty(key, value); + } + + Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer); + handler.setResult(result); + } + + private boolean isICalendarElement(Node node) { + if (node == null) { + return false; + } + + if (!(node instanceof Element)) { + return false; + } + + return XmlUtils.hasQName(node, ICALENDAR); + } + + @Override + protected void _write(ICalendar ical) throws IOException { + try { + if (!started) { + handler.startDocument(); + + if (!icalendarElementExists) { + //don't output a element if the parent is a element + start(ICALENDAR); + } + + started = true; + } + + write((ICalComponent) ical); + } catch (SAXException e) { + throw new IOException(e); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void write(ICalComponent component) throws SAXException { + ICalComponentScribe scribe = index.getComponentScribe(component); + String name = scribe.getComponentName().toLowerCase(); + + start(name); + + List properties = scribe.getProperties(component); + if (component instanceof ICalendar && component.getProperty(Version.class) == null) { + properties.add(0, new Version(targetVersion)); + } + + if (!properties.isEmpty()) { + start(PROPERTIES); + + for (Object propertyObj : properties) { + context.setParent(component); //set parent here incase a scribe resets the parent + ICalProperty property = (ICalProperty) propertyObj; + write(property); + } + + end(PROPERTIES); + } + + List subComponents = scribe.getComponents(component); + if (component instanceof ICalendar) { + //add the VTIMEZONE components that were auto-generated by TimezoneOptions + Collection tzs = getTimezoneComponents(); + for (VTimezone tz : tzs) { + if (!subComponents.contains(tz)) { + subComponents.add(0, tz); + } + } + } + if (!subComponents.isEmpty()) { + start(COMPONENTS); + for (Object subComponentObj : subComponents) { + ICalComponent subComponent = (ICalComponent) subComponentObj; + write(subComponent); + } + end(COMPONENTS); + } + + end(name); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void write(ICalProperty property) throws SAXException { + ICalPropertyScribe scribe = index.getPropertyScribe(property); + ICalParameters parameters = scribe.prepareParameters(property, context); + + //get the property element to write + Element propertyElement; + if (property instanceof Xml) { + Xml xml = (Xml) property; + Document value = xml.getValue(); + if (value == null) { + return; + } + propertyElement = value.getDocumentElement(); + } else { + QName qname = scribe.getQName(); + propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); + try { + scribe.writeXml(property, propertyElement, context); + } catch (SkipMeException e) { + return; + } + } + + start(propertyElement); + + write(parameters); + write(propertyElement); + + end(propertyElement); + } + + private void write(Element propertyElement) throws SAXException { + NodeList children = propertyElement.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + + if (child instanceof Element) { + Element element = (Element) child; + + if (element.hasChildNodes()) { + start(element); + write(element); + end(element); + } else { + childless(element); + } + + continue; + } + + if (child instanceof Text) { + Text text = (Text) child; + text(text.getTextContent()); + continue; + } + } + } + + private void write(ICalParameters parameters) throws SAXException { + if (parameters.isEmpty()) { + return; + } + + start(PARAMETERS); + + for (Map.Entry> parameter : parameters) { + String parameterName = parameter.getKey().toLowerCase(); + start(parameterName); + + for (String parameterValue : parameter.getValue()) { + ICalDataType dataType = parameterDataTypes.get(parameterName); + String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); + + start(dataTypeElementName); + text(parameterValue); + end(dataTypeElementName); + } + + end(parameterName); + } + + end(PARAMETERS); + } + + /** + * Makes an childless element appear as {@code } instead of + * {@code } + * @param element the element + * @throws SAXException if there's a problem creating the element + */ + private void childless(Element element) throws SAXException { + Attributes attributes = getElementAttributes(element); + handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes); + handler.endElement(element.getNamespaceURI(), "", element.getLocalName()); + } + + private void start(Element element) throws SAXException { + Attributes attributes = getElementAttributes(element); + start(element.getNamespaceURI(), element.getLocalName(), attributes); + } + + private void start(String element) throws SAXException { + start(element, new AttributesImpl()); + } + + private void start(QName qname) throws SAXException { + start(qname, new AttributesImpl()); + } + + private void start(QName qname, Attributes attributes) throws SAXException { + start(qname.getNamespaceURI(), qname.getLocalPart(), attributes); + } + + private void start(String element, Attributes attributes) throws SAXException { + start(XCAL_NS, element, attributes); + } + + private void start(String namespace, String element, Attributes attributes) throws SAXException { + handler.startElement(namespace, "", element, attributes); + } + + private void end(Element element) throws SAXException { + end(element.getNamespaceURI(), element.getLocalName()); + } + + private void end(String element) throws SAXException { + end(XCAL_NS, element); + } + + private void end(QName qname) throws SAXException { + end(qname.getNamespaceURI(), qname.getLocalPart()); + } + + private void end(String namespace, String element) throws SAXException { + handler.endElement(namespace, "", element); + } + + private void text(String text) throws SAXException { + handler.characters(text.toCharArray(), 0, text.length()); + } + + private static Attributes getElementAttributes(Element element) { + AttributesImpl attributes = new AttributesImpl(); + NamedNodeMap attributeNodes = element.getAttributes(); + for (int i = 0; i < attributeNodes.getLength(); i++) { + Node node = attributeNodes.item(i); + + String localName = node.getLocalName(); + if ("xmlns".equals(localName)) { + continue; + } + + attributes.addAttribute(node.getNamespaceURI(), "", node.getLocalName(), "", node.getNodeValue()); + } + return attributes; + } + + /** + * Terminates the XML document and closes the output stream. + */ + public void close() throws IOException { + try { + if (!started) { + handler.startDocument(); + + if (!icalendarElementExists) { + //don't output a element if the parent is a element + start(ICALENDAR); + } + } + + if (!icalendarElementExists) { + end(ICALENDAR); + } + handler.endDocument(); + } catch (SAXException e) { + throw new IOException(e); + } + + if (writer != null) { + writer.close(); + } + } +} diff --git a/app/src/main/java/biweekly/io/xml/XCalWriterBase.java b/app/src/main/java/biweekly/io/xml/XCalWriterBase.java new file mode 100644 index 0000000000..02825b8f7e --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/XCalWriterBase.java @@ -0,0 +1,98 @@ +package biweekly.io.xml; + +import java.util.HashMap; +import java.util.Map; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.io.StreamWriter; +import biweekly.parameter.ICalParameters; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Base class for xCal writers. + * @author Michael Angstadt + */ +abstract class XCalWriterBase extends StreamWriter { + protected final ICalVersion targetVersion = ICalVersion.V2_0; //xCal only supports 2.0 + + /** + * Defines the names of the XML elements that are used to hold each + * parameter's value. + */ + protected final Map parameterDataTypes = new HashMap(); + { + registerParameterDataType(ICalParameters.ALTREP, ICalDataType.URI); + //registerParameterDataType(ICalParameters.CHARSET, ICalDataType.TEXT); //not used by 2.0 + registerParameterDataType(ICalParameters.CN, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.CUTYPE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.DELEGATED_FROM, ICalDataType.CAL_ADDRESS); + registerParameterDataType(ICalParameters.DELEGATED_TO, ICalDataType.CAL_ADDRESS); + registerParameterDataType(ICalParameters.DIR, ICalDataType.URI); + registerParameterDataType(ICalParameters.DISPLAY, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.EMAIL, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.ENCODING, ICalDataType.TEXT); + //registerParameterDataType(ICalParameters.EXPECT, ICalDataType.TEXT); //not used by 2.0 + registerParameterDataType(ICalParameters.FEATURE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.FMTTYPE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.FBTYPE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.LABEL, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.LANGUAGE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.MEMBER, ICalDataType.CAL_ADDRESS); + registerParameterDataType(ICalParameters.PARTSTAT, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.RANGE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.RELATED, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.RELTYPE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.ROLE, ICalDataType.TEXT); + registerParameterDataType(ICalParameters.RSVP, ICalDataType.BOOLEAN); + registerParameterDataType(ICalParameters.SENT_BY, ICalDataType.CAL_ADDRESS); + //registerParameterDataType(ICalParameters.STATUS, ICalDataType.TEXT); //not used by 2.0 + //registerParameterDataType(ICalParameters.TYPE, ICalDataType.TEXT); //not used by 2.0 + registerParameterDataType(ICalParameters.TZID, ICalDataType.TEXT); + //registerParameterDataType(ICalParameters.VALUE, ICalDataType.TEXT); //not used in xCal + } + + @Override + protected ICalVersion getTargetVersion() { + return targetVersion; + } + + /** + * Registers the data type of an experimental parameter. Experimental + * parameters use the "unknown" data type by default. + * @param parameterName the parameter name (e.g. "x-foo") + * @param dataType the data type or null to remove + */ + public void registerParameterDataType(String parameterName, ICalDataType dataType) { + parameterName = parameterName.toLowerCase(); + if (dataType == null) { + parameterDataTypes.remove(parameterName); + } else { + parameterDataTypes.put(parameterName, dataType); + } + } +} diff --git a/app/src/main/java/biweekly/io/xml/package-info.java b/app/src/main/java/biweekly/io/xml/package-info.java new file mode 100644 index 0000000000..f2e73ef0d2 --- /dev/null +++ b/app/src/main/java/biweekly/io/xml/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for reading and writing xCals (XML-encoded iCalendar objects). + */ +package biweekly.io.xml; \ No newline at end of file diff --git a/app/src/main/java/biweekly/messages.properties b/app/src/main/java/biweekly/messages.properties new file mode 100644 index 0000000000..50c0ff8a0c --- /dev/null +++ b/app/src/main/java/biweekly/messages.properties @@ -0,0 +1,259 @@ +#============================================================================== +#== VALIDATION WARNINGS ======================================================= +#============================================================================== + +#parameter warnings +validate.1={0} parameter value "{1}" is non-standard. Standard values are: {2} +validate.47={0} parameter value "{1}" is deprecated in the latest iCalendar specification. +validate.53={0} parameter value ("{1}") contains one or more invalid characters. Parameter values may contain printable characters, with the exception of: {2} +validate.58={0} parameter value ("{1}") contains one or more invalid characters. Parameter values may contain printable, 7-bit ASCII characters, with the exception of: {2} +validate.54={0} parameter contains one or more invalid characters in its name. Parameter names may contain letters (A-Z), numbers (0-9), and dashes (-). +validate.57={0} parameter contains one or more invalid characters in its name. Parameter names may contain printable, 7-bit ASCII characters, with the exception of: {1} + +#generic cardinality +validate.2=A {0} property is required for this component. +validate.3=There cannot be more than one instance of the {0} property. +validate.45=iCalendar version {0} does not support this property. +validate.48=iCalendar version {0} does not support this component. + +#ICalendar +validate.4=An iCalendar object must have at least one component. +validate.44=iCalendar version 2.0 does not support having a GEO property in this location. +validate.55=Multiple {0} properties can only exist if they have different LANGUAGE parameter values. + +#Observance, VEvent, VJournal, VTodo +validate.5=The BYHOUR, BYMINUTE, and BYSECOND rule parts cannot be specified in the RecurrenceRule property when the DateStart property contains a date value (as opposed to a date-time value). +validate.6=There should be only one instance of the RecurrenceRule property. + +#VAlarm +validate.7=Audio alarms should have no more than 1 attachment. +validate.8=Email alarms must have at least one attendee. +validate.9=Only email alarms can have attendees. +validate.10=The trigger must specify which date field its duration is relative to. +validate.11=The trigger''s duration is relative to the start date, but the parent component has no start date property. +validate.12=The trigger''s duration is relative to the end date, but the parent component has no end date or duration property. + +#VEvent +validate.14=A start date must be defined if no Method property is set at the top level of the iCalendar object. +validate.17=The DateStart and DateEnd properties must have the same data type (they must either both be dates or both be date-times). +validate.18=An end date and a duration cannot both be defined in the same event. + +#VEvent, VFreeBusy +validate.15=A start date must be defined if an end date is defined. +validate.16=The start date must come before the end date. + +#VEvent, VJournal, VTodo +validate.13=Invalid status value of "{0}" for this component. Valid status values are: {1}. +validate.19=The DateStart and RecurrenceId properties must have the same data type (they must either both be dates or both be date-times)." + +#VFreeBusy +validate.20={0} properties in free/busy components must always contain a time component. + +#VTimezone +validate.21=At least one StandardTime or one DaylightSavingsTime property must be specified. + +#VTodo +validate.22=The start date must come before the due date. +validate.23=The DateStart and DateDue properties must have the same data type (they must either both be dates or both be date-times). +validate.24=A due date and a duration cannot both be defined in the same to-do component. +validate.25=A start date must be defined if a duration is defined. + +validate.26=Property has no value. + +#EnumProperty +validate.28=Property value "{0}" is non-standard. Standard values for this iCalendar version are: {1} +validate.46=Property value "{0}" is not supported by this iCalendar version. Supported versions are: {1} + +#PercentComplete +validate.29=Property value "{0}" should be between 1 and 100 inclusive. + +#RecurrenceProperty +validate.30=Frequency field is required. +validate.31="Until" and "count" fields cannot both be set. + +#RecurrenceRule +validate.32=Non-standard rule parts are not allowed in the latest iCalendar specification. + +#Trigger +validate.33=A duration or date must be defined. + +#UtcOffsetProperty, Timezone +validate.34=Minute offset must be between 0 and 59 inclusive. + +#Version +validate.35=A maximum version must be specified. + +#RequestStatus +validate.36=A status code must be defined. + +#ExceptionRule +validate.37=This property has been removed from the latest iCalendar specification. Its use should be avoided. + +#FreeBusy +#validate.38=No time periods are defined. #removed in 0.4.6 +validate.39=One or more time periods do not have start dates. +validate.40=One or more time periods do not have either an end date or a duration. + +#Geo +validate.41=Latitude is not set. +validate.42=Longitude is not set. + +#Daylight +validate.43=One or more values are null. + +#RecurrenceDates +validate.49=Property cannot contain a mix of dates and periods. +validate.50=Date values cannot contain a mix of date and date-time values. +validate.51=vCal 1.0 does not support period values. + +#RawProperty +validate.52={0} property contains one or more invalid characters in its name. Property names may contain letters (A-Z), numbers (0-9), and dashes (-). +validate.59={0} property contains one or more invalid characters in its name. Property names may contain printable, 7-bit ASCII characters, with the exception of: {1} + +#Image +validate.56=A format type should be defined when the property value is in-line binary data. + +#============================================================================== +#== PARSE WARNINGS ============================================================ +#============================================================================== + +parse.line=Line {0}\: {2} +parse.prop={1} property\: {2} +parse.lineWithProp=Line {0} ({1} property)\: {2} + +parse.0=Property has requested that it be skipped: {0} +parse.1=Property value could not be parsed. Reason: {0} +#parse.16=Property could not be unmarshalled. Unmarshalling as an XML property instead. Reason: {0} #removed in 0.6.1-SNAPSHOT +#parse.2=Ignoring END property that does not match up with any BEGIN properties. #removed in 0.6.1-SNAPSHOT +#parse.3=Skipped. {0} The entire line is: "{1}" #removed in 0.6.1-SNAPSHOT +parse.4=The following parameters are nameless: {0} +parse.5=Timezone ID "{0}" not recognized. Parsing with default timezone "{1}" instead. +parse.23=Property value is empty. One of the following XML elements are expected: {0}. +parse.31=Unable to decode quoted-printable value. Treating as plain-text. Reason: {0} +parse.32=The property''s character encoding ("{0}") is not supported by this system. {1} will be used instead. +parse.37=No VTIMEZONE component exists with an ID of \"{0}\". ID will be treated as an Olsen timezone ID instead. +parse.38=No VTIMEZONE component exists with an ID of \"{0}\". ID could not be parsed as an Olsen timezone ID either. The date will be parsed under the default timezone. +parse.39=VTIMEZONE component ignored: No TZID property defined. +parse.43=TZID \"{0}\" is formatted as an Olsen ID (begins with a forward slash), but no such Olsen ID exists. Since the TZID matches that of a VTIMEZONE component, the property will use the timezone defined by the VTIMEZONE component. + +#DateOrDateTimePropertyScribe +parse.6=Could not parse the raw date-time components out of the date string "{0}". + +#DateOrDateTimePropertyScribe, DateTimePropertyScribe +parse.17=Could not parse date-time value. + +#DurationPropertyScribe +parse.18=Could not parse duration value. + +#ExceptionDatesScribe +parse.19=Could not parse date value. + +#GeoScribe +parse.20=Could not parse GEO value. +parse.21=Could not parse latitude "{0}". +parse.22=Could not parse longitude "{0}". + +#IntegerPropertyScribe +parse.24=Could not parse integer value. + +#TriggerScribe +parse.25=Could not parse value as a date or duration. +parse.26=Could not parse duration "{0}". +parse.27=Could not parse date "{0}". + +#UtcOffsetPropertyScribe +parse.28=Could not parse offset string. + +#XmlScribe +parse.29=Could not parse value as XML. + +#RecurrencePropertyScribe +parse.7=Invalid {0} value "{1}". +parse.8=Ignoring non-numeric value found in {0} value: "{1}" +parse.36="Unable to integrate "$" operator into iCalendar data model. This data will be lost: {0} +parse.40=Invalid token: {0} +parse.41=Unrecognized frequency: {0} +parse.42=Invalid day: {0} + +#FreeBusyScribe, RecurrenceDatesScribe +parse.9=Time period has no start date. +parse.10=Time period''s start date "{0}" could not be parsed. +parse.11=Time period''s end date "{0}" could not be parsed. +parse.12=Time period''s duration "{0}" could not be parsed. +parse.13=Time period has no end date or duration. +parse.14=Time period''s end date or duration "{0}" could not be parsed. + +#RecurrenceDatesScribe +parse.15=Could not parse date: "{0}" + +#VersionScribe +parse.30=Could not parse version value. + +#DaylightScribe +parse.33=Could not parse UTC offset: {0} +parse.34=Could not parse start date: {0} +parse.35=Could not parse end date: {0} + +#============================================================================== +#== EXCEPTION MESSAGES ======================================================== +#============================================================================== + +#ICalComponent +exception.1=A problem occurred attempting to invoke the copy constructor of component class {0}. + +#ICalProperty +exception.16=Parameters object cannot be null. +exception.17=A problem occurred attempting to invoke the copy constructor of property class {0}. +exception.26={0} parameter value is malformed and could not be parsed. Retrieve its raw text values instead by calling property.getParameters().get("{0}"). + +#JCalRawWriter +exception.2=Call "writeStartComponent" first. +exception.3=Cannot write a property after calling "writeEndComponent". + +#FoldedLineWriter +exception.4=Line length must be greater than 0. +exception.5=The length of the indent string must be less than the max line length. + +#ICalRawLine +exception.6=Property name required. + +#ICalRawReader +#exception.7=Line is malformed--no colon character found. #removed in 0.5.1-SNAPSHOT + +#ICalRawWriter +#exception.8=Property name "{0}" contains one or more invalid characters. The following characters are not permitted: {1} #removed in 0.5.1-SNAPSHOT +#exception.9=Property name "{0}" begins with one or more whitespace characters, which is not permitted. #removed in 0.5.1-SNAPSHOT +#exception.10=Property "{0}" has a parameter named "{1}" whose value contains one or more invalid characters. The following characters are not permitted: {2} #removed in 0.5.1-SNAPSHOT + +#XCalOutputProperties +exception.11=Indent amount cannot be less than zero. + +#ICalTimeZone +exception.12=Unable to set the raw offset. Modify the VTIMEZONE component instead. + +#StreamWriter +exception.13=No scribes were found for the following component/property classes: {0} + +#TimezoneInfo +exception.14=VTimezone component must have a non-empty TimezoneId property. + +#FreeBusy +#exception.15=Period cannot be null. #removed in 0.4.6 + +#ListProperty +exception.18=List cannot be null. + +#DateTimeComponents +exception.19=Cannot parse date: {0} + +#Duration +exception.20=Invalid duration string: {0} + +#UtcOffset +exception.21=Offset string is not in ISO8610 format: {0} + +#DataUri +exception.22=URI scheme is not "data". +exception.23=Data portion of data URI is missing. +exception.24=Cannot parse data URI. Character set "{0}" is not supported by this JVM. +exception.25=Cannot create data URI. Character set "{0}" is not supported by this JVM. diff --git a/app/src/main/java/biweekly/parameter/CalendarUserType.java b/app/src/main/java/biweekly/parameter/CalendarUserType.java new file mode 100644 index 0000000000..4f3282c19e --- /dev/null +++ b/app/src/main/java/biweekly/parameter/CalendarUserType.java @@ -0,0 +1,77 @@ +package biweekly.parameter; + +import java.util.Collection; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the type of user an entity is. + * @author Michael Angstadt + * @see RFC 5545 p.16 + */ +public class CalendarUserType extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(CalendarUserType.class); + + public static final CalendarUserType INDIVIDUAL = new CalendarUserType("INDIVIDUAL"); + public static final CalendarUserType GROUP = new CalendarUserType("GROUP"); + public static final CalendarUserType RESOURCE = new CalendarUserType("RESOURCE"); + public static final CalendarUserType ROOM = new CalendarUserType("ROOM"); + public static final CalendarUserType UNKNOWN = new CalendarUserType("UNKNOWN"); + + private CalendarUserType(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static CalendarUserType find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static CalendarUserType get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/Display.java b/app/src/main/java/biweekly/parameter/Display.java new file mode 100644 index 0000000000..8f853a9f30 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/Display.java @@ -0,0 +1,97 @@ +package biweekly.parameter; + +import java.util.Collection; + +import biweekly.property.Image; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the ways in which an {@link Image} can be displayed. + * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.13 + */ +public class Display extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(Display.class); + + /** + * The image should be displayed in-line with the component title. + */ + public static final Display BADGE = new Display("BADGE"); + + /** + * The image can be used as a replacement for the information in the + * component. + */ + public static final Display GRAPHIC = new Display("GRAPHIC"); + + /** + * The image enhances or accompanies the information in the component. + */ + public static final Display FULLSIZE = new Display("FULLSIZE"); + + /** + * A smaller version of the {@link #FULLSIZE} image. Used when there is not + * enough space to display the {@link #FULLSIZE} image. + */ + public static final Display THUMBNAIL = new Display("THUMBNAIL"); + + private Display(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static Display find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static Display get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/Encoding.java b/app/src/main/java/biweekly/parameter/Encoding.java new file mode 100644 index 0000000000..c0512ae85e --- /dev/null +++ b/app/src/main/java/biweekly/parameter/Encoding.java @@ -0,0 +1,77 @@ +package biweekly.parameter; + +import java.util.Collection; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines how a property value is encoded. + * @author Michael Angstadt + * @see RFC 5545 p.18-9 + */ +public class Encoding extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(Encoding.class); + + public static final Encoding BASE64 = new Encoding("BASE64"); + + public static final Encoding QUOTED_PRINTABLE = new Encoding("QUOTED-PRINTABLE"); //1.0 only + + public static final Encoding _8BIT = new Encoding("8BIT"); + + private Encoding(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static Encoding find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static Encoding get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/EnumParameterValue.java b/app/src/main/java/biweekly/parameter/EnumParameterValue.java new file mode 100644 index 0000000000..a4f8cd7287 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/EnumParameterValue.java @@ -0,0 +1,58 @@ +package biweekly.parameter; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a value from a parameter that has a list of pre-defined values + * (for example, the VALUE or ACTION parameters). + * @author Michael Angstadt + */ +public class EnumParameterValue { + /** + * The value (for example, "text"). + */ + protected final String value; + + /** + * @param value the value (e.g. "text") + */ + protected EnumParameterValue(String value) { + this.value = value; + } + + /** + * Gets the value of the parameter. + * @return the value of the parameter (e.g. "text") + */ + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/app/src/main/java/biweekly/parameter/Feature.java b/app/src/main/java/biweekly/parameter/Feature.java new file mode 100644 index 0000000000..ba118ca8e0 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/Feature.java @@ -0,0 +1,111 @@ +package biweekly.parameter; + +import java.util.Collection; + +import biweekly.property.Conference; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the features of a {@link Conference}. + * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.15 + */ +public class Feature extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(Feature.class); + + /** + * The conference has audio. + */ + public static final Feature AUDIO = new Feature("AUDIO"); + + /** + * The conference has chat or instant messaging. + */ + public static final Feature CHAT = new Feature("CHAT"); + + /** + * The conference has some kind of feed, such as an Atom or RSS feed. + */ + public static final Feature FEED = new Feature("FEED"); + + /** + * Indicates that the property value is specific to the owner of the + * conference. + */ + public static final Feature MODERATOR = new Feature("MODERATOR"); + + /** + * The conference is a phone conference. + */ + public static final Feature PHONE = new Feature("PHONE"); + + /** + * The conference supports screen sharing. + */ + public static final Feature SCREEN = new Feature("SCREEN"); + + /** + * The conference is a video conference. + */ + public static final Feature VIDEO = new Feature("VIDEO"); + + private Feature(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static Feature find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static Feature get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/FreeBusyType.java b/app/src/main/java/biweekly/parameter/FreeBusyType.java new file mode 100644 index 0000000000..fbf77b3ac7 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/FreeBusyType.java @@ -0,0 +1,76 @@ +package biweekly.parameter; + +import java.util.Collection; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines whether a calendar user is free or busy over a time period. + * @author Michael Angstadt + * @see RFC 5545 p.20-1 + */ +public class FreeBusyType extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(FreeBusyType.class); + + public static final FreeBusyType FREE = new FreeBusyType("FREE"); + public static final FreeBusyType BUSY = new FreeBusyType("BUSY"); + public static final FreeBusyType BUSY_UNAVAILABLE = new FreeBusyType("BUSY-UNAVAILABLE"); + public static final FreeBusyType BUSY_TENTATIVE = new FreeBusyType("BUSY-TENTATIVE"); + + private FreeBusyType(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static FreeBusyType find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static FreeBusyType get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/ICalParameterCaseClasses.java b/app/src/main/java/biweekly/parameter/ICalParameterCaseClasses.java new file mode 100644 index 0000000000..7646d9c39f --- /dev/null +++ b/app/src/main/java/biweekly/parameter/ICalParameterCaseClasses.java @@ -0,0 +1,68 @@ +package biweekly.parameter; + +import java.lang.reflect.Constructor; + +import biweekly.ICalVersion; +import biweekly.util.CaseClasses; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Manages the list of pre-defined values for a parameter (such as VALUE or + * ENCODING). + * @author Michael Angstadt + * @param the parameter class + */ +public class ICalParameterCaseClasses extends CaseClasses { + public ICalParameterCaseClasses(Class clazz) { + super(clazz); + } + + @Override + protected T create(String value) { + //reflection: return new ClassName(value); + try { + //try (String) constructor + Constructor constructor = clazz.getDeclaredConstructor(String.class); + constructor.setAccessible(true); + return constructor.newInstance(value); + } catch (Exception e) { + try { + //try (String, ICalVersion...) constructor + Constructor constructor = clazz.getDeclaredConstructor(String.class, ICalVersion[].class); + constructor.setAccessible(true); + return constructor.newInstance(value, new ICalVersion[] {}); + } catch (Exception e2) { + throw new RuntimeException(e2); + } + } + } + + @Override + protected boolean matches(T object, String value) { + return object.value.equalsIgnoreCase(value); + } +} diff --git a/app/src/main/java/biweekly/parameter/ICalParameters.java b/app/src/main/java/biweekly/parameter/ICalParameters.java new file mode 100644 index 0000000000..c2ecd4dc6f --- /dev/null +++ b/app/src/main/java/biweekly/parameter/ICalParameters.java @@ -0,0 +1,1597 @@ +package biweekly.parameter; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.github.mangstadt.vinnie.SyntaxStyle; +import com.github.mangstadt.vinnie.validate.AllowedCharacters; +import com.github.mangstadt.vinnie.validate.VObjectValidator; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.Messages; +import biweekly.ValidationWarning; +import biweekly.property.Attendee; +import biweekly.property.Conference; +import biweekly.property.FreeBusy; +import biweekly.property.Image; +import biweekly.property.Organizer; +import biweekly.property.RecurrenceId; +import biweekly.property.RelatedTo; +import biweekly.property.Trigger; +import biweekly.util.ListMultimap; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Stores the parameters that belong to a property. + * @author Michael Angstadt + */ +public class ICalParameters extends ListMultimap { + /** + * Contains a URI that points to additional information about the entity + * represented by the property. + * @see RFC 5545 + * p.14-5 + */ + public static final String ALTREP = "ALTREP"; + + /** + * Defines the character set that the property value is encoded in (for + * example, "UTF-8"). It is only used in the vCal 1.0 standard, and is + * typically used when a property value is encoded in quoted-printable + * encoding. + * @see vCal 1.0 p.16 + */ + public static final String CHARSET = "CHARSET"; + + /** + * Contains a human-readable, display name of the entity represented by this + * property (for example, "John Doe"). It is used by the {@link Attendee} + * and {@link Organizer} properties. + * @see RFC 5545 + * p.15-6 + */ + public static final String CN = "CN"; + + /** + * Used by the {@link Attendee} property. It defines the type of object that + * the attendee is (for example, an "individual" or a "room"). + * @see RFC 5545 + * p.16 + */ + public static final String CUTYPE = "CUTYPE"; + + /** + * Used by the {@link Attendee} property. It stores a list of people who + * have delegated their responsibility to the attendee. The values must be + * URIs. They are typically email URIs (for example, + * "mailto:janedoe@example.com"). + * @see RFC 5545 + * p.17 + */ + public static final String DELEGATED_FROM = "DELEGATED-FROM"; + + /** + * Used by the {@link Attendee} property. It stores a list of people to + * which the attendee has delegated his or her responsibility. The values + * must be URIs. They are typically email URIs (for example, + * "mailto:janedoe@example.com"). + * @see RFC 5545 + * p.17-8 + */ + public static final String DELEGATED_TO = "DELEGATED-TO"; + + /** + * Contains a URI (such as an LDAP URI) which points to additional + * information about the person that the property represents. It is used by + * the {@link Attendee} and {@link Organizer} properties. + * @see RFC 5545 + * p.18 + */ + public static final String DIR = "DIR"; + + /** + * Used by the {@link Image} property. It defines the ways in which the + * client application should display the image (for example, as a + * thumbnail-sized image). + * @see + * draft-ietf-calext-extensions p.13 + */ + public static final String DISPLAY = "DISPLAY"; + + /** + * Used by the {@link Attendee} property. Normally, this property's value + * contains the email address of the attendee. But if the property value + * must hold something else, this parameter can be used to store the + * attendee's email address. + * @see + * draft-ietf-calext-extensions p.14 + */ + public static final String EMAIL = "EMAIL"; + + /** + * Defines how the property value is encoded (for example, "base64" for a + * binary value). + * @see RFC 5545 + * p.18-9 + */ + public static final String ENCODING = "ENCODING"; + + /** + * Used by the {@link Attendee} property. It defines whether the event + * organizer expects the attendee to attend or not. It is only used in the + * vCal 1.0 standard. + * @see vCal 1.0 p.25 + */ + public static final String EXPECT = "EXPECT"; + + /** + * Used by the {@link Conference} property. It defines the features that the + * conference supports (for example, audio and video). + * @see + * draft-ietf-calext-extensions p.15 + */ + public static final String FEATURE = "FEATURE"; + + /** + * Defines the content type of the property value (for example, "image/jpg" + * if the property value is a JPEG image). + * @see RFC 5545 + * p.19-20 + */ + public static final String FMTTYPE = "FMTTYPE"; + + /** + * Used by the {@link FreeBusy} property. It defines whether the person is + * "free" or "busy" over the time periods that are specified in the property + * value. If this parameter is not set, the user should be considered "busy" + * during these times. + * @see RFC 5545 + * p.20 + */ + public static final String FBTYPE = "FBTYPE"; + + /** + * Defines a human-readable label for the property. + * @see + * draft-ietf-calext-extensions-01 p.16 + */ + public static final String LABEL = "LABEL"; + + /** + * Defines the language that the property value is written in (for example, + * "en" for English). + * @see RFC 5545 + * p.21 + */ + public static final String LANGUAGE = "LANGUAGE"; + + /** + * Used by the {@link Attendee} property. It defines the groups that the + * attendee is a member of in the form of URIs. Typically, these are email + * URIs (for example, "mailto:mailinglist@example.com"). + * @see RFC 5545 + * p.21-2 + */ + public static final String MEMBER = "MEMBER"; + + /** + * Used by the {@link Attendee} property. It defines the participation + * status of the attendee (for example, "ACCEPTED"). If none is defined, + * then the property should be treated as if this parameter was set to + * "NEEDS-ACTION". + * @see RFC 5545 + * p.22 + */ + public static final String PARTSTAT = "PARTSTAT"; + + /** + * Used by the {@link RecurrenceId} property. It defines the effective range + * of recurrence instances that the property references. + * @see RFC 5545 + * p.23-4 + */ + public static final String RANGE = "RANGE"; + + /** + * Used by the {@link Trigger} property. It defines the date-time field that + * the property's duration (if specified) is relative to (for example, the + * start date or the end date). + * @see RFC 5545 + * p.24 + */ + public static final String RELATED = "RELATED"; + + /** + * Used by the {@link RelatedTo} property. It defines the kind of + * relationship the property is describing (for example, a "child" + * relationship). + * @see RFC 5545 + * p.25 + */ + public static final String RELTYPE = "RELTYPE"; + + /** + * Used by the {@link Attendee} property. It defines the attendee's role + * and/or whether they must attend or not (for example, "OPT-PARTICIPANT" + * for "optional participant"). If none is defined, then the property should + * be treated as if this parameter was set to "REQ-PARTICIPANT" (required + * participant). + * @see RFC 5545 + * p.25 + * @see vCal 1.0 p.25 + */ + public static final String ROLE = "ROLE"; + + /** + * Used by the {@link Attendee} property. It defines whether the event + * organizer would like the attendee to reply with his or her intention of + * attending ("true" if the organizer would like a reply, "false" if not). + * If this parameter is not defined, then the property should be treated as + * if this parameter was set to "false". + * @see RFC 5545 + * p.26 + * @see vCal 1.0 p.25 + */ + public static final String RSVP = "RSVP"; + + /** + * Defines a URI which represents a person who is acting on behalf of the + * person that is defined in the property. Typically, the URI is an email + * URI (for example, "mailto:janedoe@example.com"). It is used by the + * {@link Attendee} and {@link Organizer} properties. + * @see RFC 5545 + * p.27 + */ + public static final String SENT_BY = "SENT-BY"; + + /** + * Used by the {@link Attendee} property. It defines the status of the + * person's event invitation (for example, "TENTATIVE" if the person may or + * may not attend). It is only used in the vCal 1.0 standard. + * @see vCal 1.0 p.25 + */ + public static final String STATUS = "STATUS"; + + /** + * Defines the content type of the property value (for example, "WAVE" for + * an audio file). It is only used in the vCal 1.0 standard. + * @see vCal 1.0 p.27 + */ + public static final String TYPE = "TYPE"; + + /** + * Used by properties that contain date-time values. It defines the timezone + * that the property value is formatted in. It either references a timezone + * defined in a VTIMEZONE component, or contains an Olson timezone ID. To + * use an Olson timezone ID, the parameter value must be prepended with a + * "/" (for example, "/America/New_York"). + * @see RFC 5545 + * p.27-8 + */ + public static final String TZID = "TZID"; + + /** + * Defines the data type of the property value (for example, "date" if the + * property value is a date without a time component). It is used if the + * property accepts multiple values that have different data types. + * @see RFC 5545 + * p.29-50 + */ + public static final String VALUE = "VALUE"; + + /** + * Creates a parameters list. + */ + public ICalParameters() { + /* + * Initialize map size to 0 because most properties don't use any + * parameters. + */ + super(0); + } + + /** + * Copies an existing parameters list. + * @param parameters the list to copy + */ + public ICalParameters(ICalParameters parameters) { + super(parameters); + } + + /** + *

+ * Creates a parameter list that is backed by the given map. Any changes + * made to the given map will effect the parameter list and vice versa. + *

+ *

+ * Care must be taken to ensure that the given map's keys are all in + * uppercase. + *

+ *

+ * To avoid problems, it is highly recommended that the given map NOT be + * modified by anything other than this {@link ICalParameters} class after + * being passed into this constructor. + *

+ * @param map the map + */ + public ICalParameters(Map> map) { + super(map); + } + + /** + *

+ * Gets the ALTREP (alternate representation) parameter value. + *

+ *

+ * This parameter contains a URI that points to additional information about + * the entity represented by the property. + *

+ * @return the URI or null if not set + * @see RFC 5545 + * p.14-5 + */ + public String getAltRepresentation() { + return first(ALTREP); + } + + /** + *

+ * Sets the ALTREP (alternate representation) parameter value. + *

+ *

+ * This parameter contains a URI that points to additional information about + * the entity represented by the property. + *

+ * @param uri the URI or null to remove + * @see RFC 5545 + * p.14-5 + */ + public void setAltRepresentation(String uri) { + replace(ALTREP, uri); + } + + /** + *

+ * Gets the CHARSET parameter value. + *

+ *

+ * This parameter contains the character set that the property value is + * encoded in (for example, "UTF-8"). It is only used in the vCal 1.0 + * standard, and is typically used when a property value is encoded in + * quoted-printable encoding. + *

+ * @return the character set or null if not set + * @see vCal 1.0 p.16 + */ + public String getCharset() { + return first(CHARSET); + } + + /** + *

+ * Sets the CHARSET parameter value. + *

+ *

+ * This parameter contains the character set that the property value is + * encoded in (for example, "UTF-8"). It is only used in the vCal 1.0 + * standard, and is typically used when a property value is encoded in + * quoted-printable encoding. + *

+ * @param charset the character set or null to remove + * @see vCal 1.0 p.16 + */ + public void setCharset(String charset) { + replace(CHARSET, charset); + } + + /** + *

+ * Gets the CN (common name) parameter value. + *

+ *

+ * This parameter contains a human-readable, display name of the entity + * represented by this property (for example, "John Doe"). It is used by the + * {@link Attendee} and {@link Organizer} properties. + *

+ * @return the common name or null if not set + * @see RFC 5545 + * p.15-6 + */ + public String getCommonName() { + return first(CN); + } + + /** + *

+ * Sets the CN (common name) parameter value. + *

+ *

+ * This parameter contains a human-readable, display name of the entity + * represented by this property (for example, "John Doe"). It is used by the + * {@link Attendee} and {@link Organizer} properties. + *

+ * @param cn the common name or null to remove + * @see RFC 5545 + * p.15-6 + */ + public void setCommonName(String cn) { + replace(CN, cn); + } + + /** + *

+ * Gets the CUTYPE (calendar user type) parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * type of object that the attendee is (for example, an "individual" or a + * "room"). + *

+ * @return the calendar user type or null if not set + * @see RFC 5545 + * p.16 + */ + public CalendarUserType getCalendarUserType() { + String value = first(CUTYPE); + return (value == null) ? null : CalendarUserType.get(value); + } + + /** + *

+ * Sets the CUTYPE (calendar user type) parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * type of object that the attendee is (for example, an "individual" or a + * "room"). + *

+ * @param calendarUserType the calendar user type or null to remove + * @see RFC 5545 + * p.16 + */ + public void setCalendarUserType(CalendarUserType calendarUserType) { + replace(CUTYPE, (calendarUserType == null) ? null : calendarUserType.getValue()); + } + + /** + *

+ * Gets the DELEGATED-FROM parameter values. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It stores a list + * of people who have delegated their responsibility to the attendee. The + * values must be URIs. They are typically email URIs (for example, + * "mailto:janedoe@example.com"). + *

+ *

+ * Changes to the returned list will update the {@link ICalParameters} + * object, and vice versa. + *

+ * @return the URIs or an empty list if none are set + * @see RFC 5545 + * p.17 + */ + public List getDelegatedFrom() { + return get(DELEGATED_FROM); + } + + /** + *

+ * Gets the DELEGATED-TO parameter values. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It stores a list + * of people to which the attendee has delegated his or her responsibility. + * The values must be URIs. They are typically email URIs (for example, + * "mailto:janedoe@example.com"). + *

+ *

+ * Changes to the returned list will update the {@link ICalParameters} + * object, and vice versa. + *

+ * @return the URIs or an empty list if none are set + * @see RFC 5545 + * p.17-8 + */ + public List getDelegatedTo() { + return get(DELEGATED_TO); + } + + /** + *

+ * Gets the DIR (directory entry) parameter value. + *

+ *

+ * This parameter contains a URI (such as an LDAP URI) which points to + * additional information about the person that the property represents. It + * is used by the {@link Attendee} and {@link Organizer} properties. + *

+ * @return the URI or null if not set + * @see RFC 5545 + * p.18 + */ + public String getDirectoryEntry() { + return first(DIR); + } + + /** + *

+ * Sets the DIR (directory entry) parameter value. + *

+ *

+ * This parameter contains a URI (such as an LDAP URI) which points to + * additional information about the person that the property represents. It + * is used by the {@link Attendee} and {@link Organizer} properties. + *

+ * @param uri the URI or null to remove + * @see RFC 5545 + * p.18 + */ + public void setDirectoryEntry(String uri) { + replace(DIR, uri); + } + + /** + *

+ * Gets the DISPLAY parameter values. + *

+ *

+ * This parameter is used by the {@link Image} property. It defines the ways + * in which the client application should display the image (for example, as + * a thumbnail-sized image). + *

+ *

+ * Changes to the returned list will update the {@link ICalParameters} + * object, and vice versa. + *

+ * @return the display suggestions or empty list if none are defined + * @see + * draft-ietf-calext-extensions p.13 + */ + public List getDisplays() { + return new EnumParameterList(DISPLAY) { + @Override + protected Display _asObject(String value) { + return Display.get(value); + } + }; + } + + /** + *

+ * Gets the EMAIL parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. Normally, this + * property's value contains the email address of the attendee. But if the + * property value must hold something else, this parameter can be used to + * store the attendee's email address. + *

+ * @return the email or null if not set + * @see + * draft-ietf-calext-extensions p.14 + */ + public String getEmail() { + return first(EMAIL); + } + + /** + *

+ * Sets the EMAIL parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. Normally, this + * property's value contains the email address of the attendee. But if the + * property value must hold something else, this parameter can be used to + * store the attendee's email address. + *

+ * @param email the email or null to remove + * @see + * draft-ietf-calext-extensions p.14 + */ + public void setEmail(String email) { + replace(EMAIL, email); + } + + /** + *

+ * Gets the ENCODING parameter value. + *

+ *

+ * This parameter defines how the property value is encoded (for example, + * "base64" for a binary value). + *

+ * @return the encoding or null if not set + * @see RFC 5545 + * p.18-9 + */ + public Encoding getEncoding() { + String value = first(ENCODING); + return (value == null) ? null : Encoding.get(value); + } + + /** + *

+ * Sets the ENCODING parameter value. + *

+ *

+ * This parameter defines how the property value is encoded (for example, + * "base64" for a binary value). + *

+ * @param encoding the encoding or null to remove + * @see RFC 5545 + * p.18-9 + */ + public void setEncoding(Encoding encoding) { + replace(ENCODING, (encoding == null) ? null : encoding.getValue()); + } + + /** + *

+ * Gets the EXPECT parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines + * whether the event organizer expects the attendee to attend or not. It is + * only used in the vCal 1.0 standard. + *

+ * @return the attendance expectation or null if not set + * @see vCal 1.0 p.25 + */ + public String getExpect() { + return first(EXPECT); + } + + /** + *

+ * Sets the EXPECT parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines + * whether the event organizer expects the attendee to attend or not. It is + * only used in the vCal 1.0 standard. + *

+ * @param expect the attendance expectation or null if not set + * @see vCal 1.0 p.25 + */ + public void setExpect(String expect) { + replace(EXPECT, expect); + } + + /** + *

+ * Gets the FEATURE parameter values. + *

+ *

+ * This parameter is used by the {@link Conference} property. It defines the + * features that the conference supports (for example, audio and video). + *

+ *

+ * Changes to the returned list will update the {@link ICalParameters} + * object, and vice versa. + *

+ * @return the features or empty list if none are set + * @see + * draft-ietf-calext-extensions p.15 + */ + public List getFeatures() { + return new EnumParameterList(FEATURE) { + @Override + protected Feature _asObject(String value) { + return Feature.get(value); + } + }; + } + + /** + *

+ * Gets the FMTTYPE (format type) parameter value. + *

+ *

+ * This parameter defines the content type of the property value (for + * example, "image/jpg" if the property value is a JPEG image). + *

+ * @return the format type or null if not set + * @see RFC 5545 + * p.19-20 + */ + public String getFormatType() { + return first(FMTTYPE); + } + + /** + *

+ * Sets the FMTTYPE (format type) parameter value. + *

+ *

+ * This parameter defines the content type of the property value (for + * example, "image/jpg" if the property value is a JPEG image). + *

+ * @param formatType the format type or null to remove + * @see RFC 5545 + * p.19-20 + */ + public void setFormatType(String formatType) { + replace(FMTTYPE, formatType); + } + + /** + *

+ * Gets the FBTYPE (free busy type) parameter value. + *

+ *

+ * This parameter is used by the {@link FreeBusy} property. It defines + * whether the person is "free" or "busy" over the time periods that are + * specified in the property value. If this parameter is not set, the user + * should be considered "busy" during these times. + *

+ * @return the free busy type or null if not set + * @see RFC 5545 + * p.20 + */ + public FreeBusyType getFreeBusyType() { + String value = first(FBTYPE); + return (value == null) ? null : FreeBusyType.get(value); + } + + /** + *

+ * Sets the FBTYPE (free busy type) parameter value. + *

+ *

+ * This parameter is used by the {@link FreeBusy} property. It defines + * whether the person is "free" or "busy" over the time periods that are + * specified in the property value. If this parameter is not set, the user + * should be considered "busy" during these times. + *

+ * @param freeBusyType the free busy type or null to remove + * @see RFC 5545 + * p.20 + */ + public void setFreeBusyType(FreeBusyType freeBusyType) { + replace(FBTYPE, (freeBusyType == null) ? null : freeBusyType.getValue()); + } + + /** + *

+ * Gets the LABEL parameter value. + *

+ *

+ * This parameter defines a human-readable label for the property. + *

+ * @return the label or null if not set + * @see + * draft-ietf-calext-extensions-01 p.16 + */ + public String getLabel() { + return first(LABEL); + } + + /** + *

+ * Sets the LABEL parameter value. + *

+ *

+ * This parameter defines a human-readable label for the property. + *

+ * @param label the label or null to remove + * @see + * draft-ietf-calext-extensions-01 p.16 + */ + public void setLabel(String label) { + replace(LABEL, label); + } + + /** + *

+ * Gets the LANGUAGE parameter value. + *

+ *

+ * This parameter defines the language that the property value is written in + * (for example, "en" for English). + *

+ * @return the language or null if not set + * @see RFC 5545 + * p.21 + */ + public String getLanguage() { + return first(LANGUAGE); + } + + /** + *

+ * Sets the LANGUAGE parameter value. + *

+ *

+ * This parameter defines the language that the property value is written in + * (for example, "en" for English). + *

+ * @param language the language or null to remove + * @see RFC 5545 + * p.21 + */ + public void setLanguage(String language) { + replace(LANGUAGE, language); + } + + /** + *

+ * Gets the MEMBER property values. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * groups that the attendee is a member of in the form of URIs. Typically, + * these are email URIs (for example, "mailto:mailinglist@example.com"). + *

+ *

+ * Changes to the returned list will update the {@link ICalParameters} + * object, and vice versa. + *

+ * @return the groups or empty list if none are set + * @see RFC 5545 + * p.21-2 + */ + public List getMembers() { + return get(MEMBER); + } + + /** + *

+ * Gets the PARTSTAT (participation status) parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * participation status of the attendee (for example, "ACCEPTED"). If none + * is defined, then the property should be treated as if this parameter was + * set to "NEEDS-ACTION". + *

+ * @return the participation status or null if not set + * @see RFC 5545 + * p.22 + */ + public String getParticipationStatus() { + return first(PARTSTAT); + } + + /** + *

+ * Gets the PARTSTAT (participation status) parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * participation status of the attendee (for example, "ACCEPTED"). If none + * is defined, then the property should be treated as if this parameter was + * set to "NEEDS-ACTION". + *

+ * @param participationStatus the participation status or null to remove + * @see RFC 5545 + * p.22 + */ + public void setParticipationStatus(String participationStatus) { + replace(PARTSTAT, participationStatus); + } + + /** + *

+ * Gets the RANGE parameter value. + *

+ *

+ * This parameter is used by the {@link RecurrenceId} property. It defines + * the effective range of recurrence instances that the property references. + *

+ * @return the range or null if not set + * @see RFC 5545 + * p.23-4 + */ + public Range getRange() { + String value = first(RANGE); + return (value == null) ? null : Range.get(value); + } + + /** + *

+ * Sets the RANGE parameter value. + *

+ *

+ * This parameter is used by the {@link RecurrenceId} property. It defines + * the effective range of recurrence instances that the property references. + *

+ * @param range the range or null to remove + * @see RFC 5545 + * p.23-4 + */ + public void setRange(Range range) { + replace(RANGE, (range == null) ? null : range.getValue()); + } + + /** + *

+ * Gets the RELATED parameter value. + *

+ *

+ * This parameter is used by the {@link Trigger} property. It defines the + * date-time field that the property's duration (if specified) is relative + * to (for example, the start date or the end date). + *

+ * @return the related field or null if not set + * @see RFC 5545 + * p.24 + */ + public Related getRelated() { + String value = first(RELATED); + return (value == null) ? null : Related.get(value); + } + + /** + *

+ * Sets the RELATED parameter value. + *

+ *

+ * This parameter is used by the {@link Trigger} property. It defines the + * date-time field that the property's duration (if specified) is relative + * to (for example, the start date or the end date). + *

+ * @param related the related field or null to remove + * @see RFC 5545 + * p.24 + */ + public void setRelated(Related related) { + replace(RELATED, (related == null) ? null : related.getValue()); + } + + /** + *

+ * Gets the RELTYPE (relationship type) parameter value. + *

+ *

+ * This parameter is used by the {@link RelatedTo} property. It defines the + * kind of relationship the property is describing (for example, a "child" + * relationship). + *

+ * @return the relationship type or null if not set + * @see RFC 5545 + * p.25 + */ + public RelationshipType getRelationshipType() { + String value = first(RELTYPE); + return (value == null) ? null : RelationshipType.get(value); + } + + /** + *

+ * Sets the RELTYPE (relationship type) parameter value. + *

+ *

+ * This parameter is used by the {@link RelatedTo} property. It defines the + * kind of relationship the property is describing (for example, a "child" + * relationship). + *

+ * @param relationshipType the relationship type or null to remove + * @see RFC 5545 + * p.25 + */ + public void setRelationshipType(RelationshipType relationshipType) { + replace(RELTYPE, (relationshipType == null) ? null : relationshipType.getValue()); + } + + /** + *

+ * Gets the ROLE parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * attendee's role and/or whether they must attend or not (for example, + * "OPT-PARTICIPANT" for "optional participant"). If none is defined, then + * the property should be treated as if this parameter was set to + * "REQ-PARTICIPANT" (required participant). + *

+ * @return the role or null if not set + * @see RFC 5545 + * p.25 + * @see vCal 1.0 p.25 + */ + public String getRole() { + /* + * Note: The acceptable values for this parameter differs in vCal 1.0, + * which is why this method does not return an enum. + */ + return first(ROLE); + } + + /** + *

+ * Sets the ROLE parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * attendee's role and/or whether they must attend or not (for example, + * "OPT-PARTICIPANT" for "optional participant"). If none is defined, then + * the property should be treated as if this parameter was set to + * "REQ-PARTICIPANT" (required participant). + *

+ * @param role the role or null to remove + * @see RFC 5545 + * p.25 + * @see vCal 1.0 p.25 + */ + public void setRole(String role) { + replace(ROLE, role); + } + + /** + *

+ * Gets the RSVP parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines + * whether the event organizer would like the attendee to reply with his or + * her intention of attending ("true" if the organizer would like a reply, + * "false" if not). If this parameter is not defined, then the property + * should be treated as if this parameter was set to "false". + *

+ * @return the value or null if not set + * @see RFC 5545 + * p.26 + * @see vCal 1.0 p.25 + */ + public String getRsvp() { + /* + * Note: The acceptable values for this parameter differs in vCal 1.0, + * which is why this method does not return a boolean. + */ + return first(RSVP); + } + + /** + *

+ * Sets the RSVP parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines + * whether the event organizer would like the attendee to reply with his or + * her intention of attending ("true" if the organizer would like a reply, + * "false" if not). If this parameter is not defined, then the property + * should be treated as if this parameter was set to "false". + *

+ * @param rsvp the value or null to remove + * @see RFC 5545 + * p.26 + * @see vCal 1.0 p.25 + */ + public void setRsvp(String rsvp) { + replace(RSVP, rsvp); + } + + /** + *

+ * Gets the SENT-BY parameter value. + *

+ *

+ * This parameter defines a URI which represents a person who is acting on + * behalf of the person that is defined in the property. Typically, the URI + * is an email URI (for example, "mailto:janedoe@example.com"). It is used + * by the {@link Attendee} and {@link Organizer} properties. + *

+ * @return the URI or null if not set + * @see RFC 5545 + * p.27 + */ + public String getSentBy() { + return first(SENT_BY); + } + + /** + *

+ * Sets the SENT-BY parameter value. + *

+ *

+ * This parameter defines a URI which represents a person who is acting on + * behalf of the person that is defined in the property. Typically, the URI + * is an email URI (for example, "mailto:janedoe@example.com"). It is used + * by the {@link Attendee} and {@link Organizer} properties. + *

+ * @param uri the URI or null to remove + * @see RFC 5545 + * p.27 + */ + public void setSentBy(String uri) { + replace(SENT_BY, uri); + } + + /** + *

+ * Gets the STATUS parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * status of the person's event invitation (for example, "TENTATIVE" if the + * person may or may not attend). It is only used in the vCal 1.0 standard. + *

+ * @return the status or null if not set + * @see vCal 1.0 p.25 + */ + public String getStatus() { + return first(STATUS); + } + + /** + *

+ * Sets the STATUS parameter value. + *

+ *

+ * This parameter is used by the {@link Attendee} property. It defines the + * status of the person's event invitation (for example, "TENTATIVE" if the + * person may or may not attend). It is only used in the vCal 1.0 standard. + *

+ * @param status the status or null to remove + * @see vCal 1.0 p.25 + */ + public void setStatus(String status) { + replace(STATUS, status); + } + + /** + *

+ * Gets the TZID (timezone ID) parameter value. + *

+ *

+ * This parameter is used by properties that contain date-time values. It + * defines the timezone that the property value is formatted in. It either + * references a timezone defined in a VTIMEZONE component, or contains an + * Olson timezone ID. To use an Olson timezone ID, the parameter value must + * be prepended with a "/" (for example, "/America/New_York"). + *

+ * @return the timezone ID or null if not set + * @see RFC 5545 + * p.27-8 + */ + public String getTimezoneId() { + return first(TZID); + } + + /** + *

+ * Sets the TZID (timezone ID) parameter value. + *

+ *

+ * This parameter is used by properties that contain date-time values. It + * defines the timezone that the property value is formatted in. It either + * references a timezone defined in a VTIMEZONE component, or contains an + * Olson timezone ID. To use an Olson timezone ID, the parameter value must + * be prepended with a "/" (for example, "/America/New_York"). + *

+ * @param timezoneId the timezone ID or null to remove + * @see RFC 5545 + * p.27-8 + */ + public void setTimezoneId(String timezoneId) { + replace(TZID, timezoneId); + } + + /** + *

+ * Gets the TYPE parameter value. + *

+ *

+ * This parameter defines the content type of the property value (for + * example, "WAVE" for an audio file). It is only used in the vCal 1.0 + * standard. + *

+ * @return the type or null if not set + * @see vCal 1.0 p.27 + */ + public String getType() { + return first(TYPE); + } + + /** + *

+ * Sets the TYPE parameter value. + *

+ *

+ * This parameter defines the content type of the property value (for + * example, "WAVE" for an audio file). It is only used in the vCal 1.0 + * standard. + *

+ * @param type the type or null to remove + * @see vCal 1.0 p.27 + */ + public void setType(String type) { + replace(TYPE, type); + } + + /** + *

+ * Gets the VALUE parameter value. + *

+ *

+ * This parameter defines the data type of the property value (for example, + * "date" if the property value is a date without a time component). It is + * used if the property accepts multiple values that have different data + * types. + *

+ * @return the data type or null if not set + * @see RFC 5545 + * p.29-50 + */ + public ICalDataType getValue() { + String value = first(VALUE); + return (value == null) ? null : ICalDataType.get(value); + } + + /** + *

+ * Sets the VALUE parameter value. + *

+ *

+ * This parameter defines the data type of the property value (for example, + * "date" if the property value is a date without a time component). It is + * used if the property accepts multiple values that have different data + * types. + *

+ * @param dataType the data type or null to remove + * @see RFC 5545 + * p.29-50 + */ + public void setValue(ICalDataType dataType) { + replace(VALUE, (dataType == null) ? null : dataType.getName()); + } + + /** + *

+ * Checks the parameters for data consistency problems or deviations from + * the specification. + *

+ *

+ * These problems will not prevent the iCalendar object from being written + * to a data stream*, but may prevent it from being parsed correctly by the + * consuming application. + *

+ *

+ * *With a few exceptions: One thing this method does is check for illegal + * characters. There are certain characters that will break the iCalendar + * syntax if written (such as a newline character in a parameter name). If + * one of these characters is present, it WILL prevent the iCalendar object + * from being written. + *

+ * @param version the version to validate against + * @return a list of warnings or an empty list if no problems were found + */ + public List validate(ICalVersion version) { + List warnings = new ArrayList(0); + + SyntaxStyle syntax; + switch (version) { + case V1_0: + syntax = SyntaxStyle.OLD; + break; + default: + syntax = SyntaxStyle.NEW; + break; + } + + /* + * Check for invalid characters in names and values. + */ + for (Map.Entry> entry : this) { + String name = entry.getKey(); + + //check the parameter name + if (!VObjectValidator.validateParameterName(name, syntax, true)) { + if (syntax == SyntaxStyle.OLD) { + AllowedCharacters notAllowed = VObjectValidator.allowedCharactersParameterName(syntax, true).flip(); + warnings.add(new ValidationWarning(57, name, notAllowed.toString(true))); + } else { + warnings.add(new ValidationWarning(54, name)); + } + } + + //check the parameter value(s) + List values = entry.getValue(); + for (String value : values) { + if (!VObjectValidator.validateParameterValue(value, syntax, false, true)) { + AllowedCharacters notAllowed = VObjectValidator.allowedCharactersParameterValue(syntax, false, true).flip(); + int code = (syntax == SyntaxStyle.OLD) ? 58 : 53; + warnings.add(new ValidationWarning(code, name, value, notAllowed.toString(true))); + } + } + } + + final int nonStandardCode = 1, deprecated = 47; + + String value = first(RSVP); + if (value != null) { + value = value.toLowerCase(); + List validValues = Arrays.asList("true", "false", "yes", "no"); + if (!validValues.contains(value)) { + warnings.add(new ValidationWarning(nonStandardCode, RSVP, value, validValues)); + } + } + + value = first(CUTYPE); + if (value != null && CalendarUserType.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, CUTYPE, value, CalendarUserType.all())); + } + + value = first(ENCODING); + if (value != null && Encoding.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, ENCODING, value, Encoding.all())); + } + + value = first(FBTYPE); + if (value != null && FreeBusyType.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, FBTYPE, value, FreeBusyType.all())); + } + + value = first(PARTSTAT); + if (value != null && ParticipationStatus.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, PARTSTAT, value, ParticipationStatus.all())); + } + + value = first(RANGE); + if (value != null) { + Range range = Range.find(value); + + if (range == null) { + warnings.add(new ValidationWarning(nonStandardCode, RANGE, value, Range.all())); + } + + if (range == Range.THIS_AND_PRIOR && version == ICalVersion.V2_0) { + warnings.add(new ValidationWarning(deprecated, RANGE, value)); + } + } + + value = first(RELATED); + if (value != null && Related.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, RELATED, value, Related.all())); + } + + value = first(RELTYPE); + if (value != null && RelationshipType.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, RELTYPE, value, RelationshipType.all())); + } + + value = first(ROLE); + if (value != null && Role.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, ROLE, value, Role.all())); + } + + value = first(VALUE); + if (value != null && ICalDataType.find(value) == null) { + warnings.add(new ValidationWarning(nonStandardCode, VALUE, value, ICalDataType.all())); + } + + return warnings; + } + + @Override + protected String sanitizeKey(String key) { + return (key == null) ? null : key.toUpperCase(); + } + + @Override + public int hashCode() { + /* + * Remember: Keys are case-insensitive, key order does not matter, and + * value order does not matter + */ + final int prime = 31; + int result = 1; + + for (Map.Entry> entry : this) { + String key = entry.getKey(); + List value = entry.getValue(); + + int valueHash = 1; + for (String v : value) { + valueHash += v.toLowerCase().hashCode(); + } + + int entryHash = 1; + entryHash += prime * entryHash + ((key == null) ? 0 : key.toLowerCase().hashCode()); + entryHash += prime * entryHash + valueHash; + + result += entryHash; + } + + return result; + } + + /** + *

+ * Determines whether the given object is logically equivalent to this list + * of parameters. + *

+ *

+ * Note that iCalendar parameter names are case-insensitive. Also, note that + * the order in which they are defined does not matter. + *

+ * @param obj the object to compare to + * @return true if the objects are equal, false if not + */ + @Override + public boolean equals(Object obj) { + /* + * Remember: Keys are case-insensitive, key order does not matter, and + * value order does not matter + */ + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + ICalParameters other = (ICalParameters) obj; + if (size() != other.size()) return false; + + for (Map.Entry> entry : this) { + String key = entry.getKey(); + List value = entry.getValue(); + List otherValue = other.get(key); + + if (value.size() != otherValue.size()) { + return false; + } + + List valueLower = new ArrayList(value.size()); + for (String v : value) { + valueLower.add(v.toLowerCase()); + } + Collections.sort(valueLower); + + List otherValueLower = new ArrayList(otherValue.size()); + for (String v : otherValue) { + otherValueLower.add(v.toLowerCase()); + } + Collections.sort(otherValueLower); + + if (!valueLower.equals(otherValueLower)) { + return false; + } + } + + return true; + } + + /** + *

+ * A list that converts the raw string values of a parameter to the + * appropriate {@link EnumParameterValue} object that some parameters use. + *

+ *

+ * This list is backed by the {@link ICalParameters} object. Any changes + * made to the list will affect the {@link ICalParameters} object and vice + * versa. + *

+ * @param the enum parameter class + */ + public abstract class EnumParameterList extends ICalParameterList { + public EnumParameterList(String parameterName) { + super(parameterName); + } + + @Override + protected String _asString(T value) { + return value.getValue(); + } + } + + /** + *

+ * A list that converts the raw string values of a parameter to another kind + * of value (for example, Integers). + *

+ *

+ * This list is backed by the {@link ICalParameters} object. Any changes + * made to the list will affect the {@link ICalParameters} object and vice + * versa. + *

+ *

+ * If a String value cannot be converted to the appropriate data type, an + * {@link IllegalStateException} is thrown. + *

+ */ + public abstract class ICalParameterList extends AbstractList { + protected final String parameterName; + protected final List parameterValues; + + /** + * @param parameterName the name of the parameter (case insensitive) + */ + public ICalParameterList(String parameterName) { + this.parameterName = parameterName; + parameterValues = ICalParameters.this.get(parameterName); + } + + @Override + public void add(int index, T value) { + String valueStr = _asString(value); + parameterValues.add(index, valueStr); + } + + @Override + public T remove(int index) { + String removed = parameterValues.remove(index); + return asObject(removed); + } + + @Override + public T get(int index) { + String value = parameterValues.get(index); + return asObject(value); + } + + @Override + public T set(int index, T value) { + String valueStr = _asString(value); + String replaced = parameterValues.set(index, valueStr); + return asObject(replaced); + } + + @Override + public int size() { + return parameterValues.size(); + } + + private T asObject(String value) { + try { + return _asObject(value); + } catch (Exception e) { + throw new IllegalStateException(Messages.INSTANCE.getExceptionMessage(26, parameterName), e); + } + } + + /** + * Converts the object to a String value for storing in the + * {@link ICalParameters} object. + * @param value the value + * @return the string value + */ + protected abstract String _asString(T value); + + /** + * Converts a String value to its object form. + * @param value the string value + * @return the object + * @throws Exception if there is a problem parsing the string + */ + protected abstract T _asObject(String value) throws Exception; + } +} diff --git a/app/src/main/java/biweekly/parameter/ParticipationLevel.java b/app/src/main/java/biweekly/parameter/ParticipationLevel.java new file mode 100644 index 0000000000..664e20179b --- /dev/null +++ b/app/src/main/java/biweekly/parameter/ParticipationLevel.java @@ -0,0 +1,156 @@ +package biweekly.parameter; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.util.CaseClasses; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines what level of participation is expected from a calendar user. Note + * that this class does not correspond to a particular parameter. The parameter + * varies depending on the iCalendar version. + * @author Michael Angstadt + * @see RFC 5545 p.25-6 + * @see vCal 1.0 p.26-7 + */ +public class ParticipationLevel { + private static final CaseClasses enums = new CaseClasses(ParticipationLevel.class) { + @Override + protected ParticipationLevel create(String value) { + return new ParticipationLevel(value); + } + + @Override + protected boolean matches(ParticipationLevel object, String value) { + for (String v : object.values.values()) { + if (v.equalsIgnoreCase(value)) { + return true; + } + } + return false; + } + }; + + /** + * Indicates that the user's participation is required. + */ + public static final ParticipationLevel REQUIRED; + static { + Map values = new HashMap(); + values.put(ICalVersion.V1_0, "REQUIRE"); + values.put(ICalVersion.V2_0_DEPRECATED, "REQ-PARTICIPANT"); + values.put(ICalVersion.V2_0, values.get(ICalVersion.V2_0_DEPRECATED)); + REQUIRED = new ParticipationLevel(values); + } + + /** + * Indicates that the user's participation is optional. + */ + public static final ParticipationLevel OPTIONAL; + static { + Map values = new HashMap(); + values.put(ICalVersion.V1_0, "REQUEST"); + values.put(ICalVersion.V2_0_DEPRECATED, "OPT-PARTICIPANT"); + values.put(ICalVersion.V2_0, values.get(ICalVersion.V2_0_DEPRECATED)); + OPTIONAL = new ParticipationLevel(values); + } + + /** + * Indicates that the user has been notified about the event for + * informational purposes only and does not need to attend. + */ + public static final ParticipationLevel FYI; + static { + Map values = new HashMap(); + values.put(ICalVersion.V1_0, "FYI"); + values.put(ICalVersion.V2_0_DEPRECATED, "NON-PARTICIPANT"); + values.put(ICalVersion.V2_0, values.get(ICalVersion.V2_0_DEPRECATED)); + FYI = new ParticipationLevel(values); + } + + private final Map values; + + private ParticipationLevel(Map values) { + this.values = Collections.unmodifiableMap(values); + } + + private ParticipationLevel(String value) { + Map values = new HashMap(); + for (ICalVersion version : ICalVersion.values()) { + values.put(version, value); + } + this.values = Collections.unmodifiableMap(values); + } + + /** + * Gets the value of the parameter + * @param version the version + * @return the parameter value + */ + public String getValue(ICalVersion version) { + return values.get(version); + } + + @Override + public String toString() { + return getValue(ICalVersion.V2_0); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static ParticipationLevel find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static ParticipationLevel get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/ParticipationStatus.java b/app/src/main/java/biweekly/parameter/ParticipationStatus.java new file mode 100644 index 0000000000..1430ff12c4 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/ParticipationStatus.java @@ -0,0 +1,139 @@ +package biweekly.parameter; + +import java.util.Collection; + +import biweekly.ICalVersion; +import biweekly.component.VEvent; +import biweekly.component.VJournal; +import biweekly.component.VTodo; +import biweekly.property.Attendee; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines a calendar user's level of participation. Used with the + * {@link Attendee} property. + * @author Michael Angstadt + * @see RFC 5545 p.22-3 + * @see vCal 1.0 p.25-6 + */ +public class ParticipationStatus extends VersionedEnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(ParticipationStatus.class); + + /** + * Indicates that the user needs to make a decision about the item. Valid + * within the {@link VEvent}, {@link VTodo}, {@link VJournal} components. + */ + public static final ParticipationStatus NEEDS_ACTION = new ParticipationStatus("NEEDS-ACTION"); + + /** + * Indicates that the user has accepted the item. Valid within the + * {@link VEvent}, {@link VTodo}, {@link VJournal} components. + */ + public static final ParticipationStatus ACCEPTED = new ParticipationStatus("ACCEPTED"); + + /** + * Indicates that the user has declined the item. Valid within the + * {@link VEvent}, {@link VTodo}, {@link VJournal} components. + */ + public static final ParticipationStatus DECLINED = new ParticipationStatus("DECLINED"); + + /** + * Indicates that the user has tentatively accepted the item. Valid within + * the {@link VEvent} and {@link VJournal} components. + */ + public static final ParticipationStatus TENTATIVE = new ParticipationStatus("TENTATIVE"); + + /** + * Indicates that the user has delegated the item to someone else. Valid + * within the {@link VEvent} and {@link VTodo} components. + */ + public static final ParticipationStatus DELEGATED = new ParticipationStatus("DELEGATED"); + + /** + * Indicates that the user has completed the item. Only valid within the + * {@link VTodo} component. + */ + public static final ParticipationStatus COMPLETED = new ParticipationStatus("COMPLETED"); + + /** + * Indicates that the user is in the process of completing the item. Only + * valid within the {@link VTodo} component. + */ + public static final ParticipationStatus IN_PROCESS = new ParticipationStatus("IN_PROCESS", ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + + /** + * Indicates that the user confirmed attendance. Only valid within the + * {@link VEvent} component of vCalendar version 1.0. + */ + public static final ParticipationStatus CONFIRMED = new ParticipationStatus("CONFIRMED", ICalVersion.V1_0); + + /** + * Indicates that the item was sent out to the user. Valid within + * {@link VEvent} and {@link VTodo} components of vCalendar version 1.0. + */ + public static final ParticipationStatus SENT = new ParticipationStatus("SENT", ICalVersion.V1_0); + + private ParticipationStatus(String value, ICalVersion... supportedVersions) { + super(value, supportedVersions); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static ParticipationStatus find(String value) { + if ("NEEDS ACTION".equalsIgnoreCase(value)) { //vCal + return NEEDS_ACTION; + } + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static ParticipationStatus get(String value) { + if ("NEEDS ACTION".equalsIgnoreCase(value)) { //vCal + return NEEDS_ACTION; + } + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/Range.java b/app/src/main/java/biweekly/parameter/Range.java new file mode 100644 index 0000000000..c479fb51fe --- /dev/null +++ b/app/src/main/java/biweekly/parameter/Range.java @@ -0,0 +1,81 @@ +package biweekly.parameter; + +import java.util.Collection; + +import biweekly.property.RecurrenceId; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the effective range of recurrence instances specified by the + * {@link RecurrenceId} property. This parameter is essentially deprecated. + * @author Michael Angstadt + * @see RFC 5545 p.23-4 + */ +public class Range extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(Range.class); + + public static final Range THIS_AND_FUTURE = new Range("THISANDFUTURE"); + + /** + * Deprecated in the latest iCal specification. + */ + public static final Range THIS_AND_PRIOR = new Range("THISANDPRIOR"); + + private Range(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static Range find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static Range get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/Related.java b/app/src/main/java/biweekly/parameter/Related.java new file mode 100644 index 0000000000..9f72aecd89 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/Related.java @@ -0,0 +1,75 @@ +package biweekly.parameter; + +import java.util.Collection; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the date that an alarm trigger is relative to. + * @author Michael Angstadt + * @see RFC 5545 p.24 + */ +public class Related extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(Related.class); + + public static final Related START = new Related("START"); + + public static final Related END = new Related("END"); + + private Related(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static Related find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static Related get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/RelationshipType.java b/app/src/main/java/biweekly/parameter/RelationshipType.java new file mode 100644 index 0000000000..4681729ac0 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/RelationshipType.java @@ -0,0 +1,78 @@ +package biweekly.parameter; + +import java.util.Collection; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the hierarchical relationship that a component has with another + * component. + * @author Michael Angstadt + * @see RFC 5545 p.25 + */ +public class RelationshipType extends EnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(RelationshipType.class); + + public static final RelationshipType PARENT = new RelationshipType("PARENT"); + + public static final RelationshipType CHILD = new RelationshipType("CHILD"); + + public static final RelationshipType SIBLING = new RelationshipType("SIBLING"); + + private RelationshipType(String value) { + super(value); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static RelationshipType find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static RelationshipType get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/Role.java b/app/src/main/java/biweekly/parameter/Role.java new file mode 100644 index 0000000000..01b8d2aac8 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/Role.java @@ -0,0 +1,124 @@ +package biweekly.parameter; + +import java.util.Collection; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines the role that a calendar user holds. + * @author Michael Angstadt + * @see RFC 5545 p.25-6 + * @see vCal 1.0 p.25 + */ +public class Role extends VersionedEnumParameterValue { + private static final ICalParameterCaseClasses enums = new ICalParameterCaseClasses(Role.class); + + /** + *

+ * Indicates that the user is the chair of the calendar entity. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ */ + public static final Role CHAIR = new Role("CHAIR", ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + + /** + *

+ * Indicates that the user is an attendee of the calendar entity. + *

+ *

+ * Supported versions: {@code 1.0} + *

+ */ + public static final Role ATTENDEE = new Role("ATTENDEE", ICalVersion.V1_0); + + /** + *

+ * Indicates that the user is the organizer of the calendar entity. + *

+ *

+ * Supported versions: {@code 1.0} + *

+ */ + public static final Role ORGANIZER = new Role("ORGANIZER", ICalVersion.V1_0); + + /** + *

+ * Indicates that the user is the owner of the calendar entity. + *

+ *

+ * Supported versions: {@code 1.0} + *

+ */ + public static final Role OWNER = new Role("OWNER", ICalVersion.V1_0); + + /** + *

+ * Indicates that the user is a delegate of another attendee. + *

+ *

+ * Supported versions: {@code 1.0} + *

+ */ + public static final Role DELEGATE = new Role("DELEGATE", ICalVersion.V1_0); + + private Role(String value, ICalVersion... versions) { + super(value, versions); + } + + /** + * Searches for a parameter value that is defined as a static constant in + * this class. + * @param value the parameter value + * @return the object or null if not found + */ + public static Role find(String value) { + return enums.find(value); + } + + /** + * Searches for a parameter value and creates one if it cannot be found. All + * objects are guaranteed to be unique, so they can be compared with + * {@code ==} equality. + * @param value the parameter value + * @return the object + */ + public static Role get(String value) { + return enums.get(value); + } + + /** + * Gets all of the parameter values that are defined as static constants in + * this class. + * @return the parameter values + */ + public static Collection all() { + return enums.all(); + } +} diff --git a/app/src/main/java/biweekly/parameter/VersionedEnumParameterValue.java b/app/src/main/java/biweekly/parameter/VersionedEnumParameterValue.java new file mode 100644 index 0000000000..e8a51bf058 --- /dev/null +++ b/app/src/main/java/biweekly/parameter/VersionedEnumParameterValue.java @@ -0,0 +1,62 @@ +package biweekly.parameter; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. + */ + +/** + * Represents a parameter whose values are supported by a variety of different + * iCalendar versions. + * @author Michael Angstadt + */ +public class VersionedEnumParameterValue extends EnumParameterValue { + private static final ICalVersion[] allVersions = ICalVersion.values(); + protected final ICalVersion[] supportedVersions; + + public VersionedEnumParameterValue(String value, ICalVersion... supportedVersions) { + super(value); + this.supportedVersions = (supportedVersions.length == 0) ? allVersions : supportedVersions; + } + + /** + * Determines if the parameter value is supported by the given iCalendar + * version. + * @param version the iCalendar version + * @return true if it is supported, false if not + */ + public boolean isSupported(ICalVersion version) { + for (ICalVersion supportedVersion : supportedVersions) { + if (supportedVersion == version) { + return true; + } + } + return false; + } +} diff --git a/app/src/main/java/biweekly/parameter/package-info.java b/app/src/main/java/biweekly/parameter/package-info.java new file mode 100644 index 0000000000..66e11d67cb --- /dev/null +++ b/app/src/main/java/biweekly/parameter/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes related to property parameters. + */ +package biweekly.parameter; \ No newline at end of file diff --git a/app/src/main/java/biweekly/property/Action.java b/app/src/main/java/biweekly/property/Action.java new file mode 100644 index 0000000000..1bd3fc769b --- /dev/null +++ b/app/src/main/java/biweekly/property/Action.java @@ -0,0 +1,191 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the type of action to invoke when an alarm is triggered. + *

+ * + *

+ * Code sample (creating): + *

+ * + *
+ * Action action = Action.audio();
+ * 
+ * + *

+ * Code sample (retrieving): + *

+ * + *
+ * ICalendar ical = ...
+ * for (VAlarm alarm : ical.getAlarms()) {
+ *   Action action = alarm.getAction();
+ *   if (action.isAudio()) {
+ *     //...
+ *   } else if (action.isEmail()) {
+ *     //...
+ *   } else if (action.isDisplay()) {
+ *     //...
+ *   }
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.132-3 + * @see RFC 2445 p.126 + */ +public class Action extends EnumProperty { + public static final String AUDIO = "AUDIO"; + public static final String DISPLAY = "DISPLAY"; + public static final String EMAIL = "EMAIL"; + public static final String PROCEDURE = "PROCEDURE"; + + /** + * Creates an action property. Use of this constructor is discouraged and + * may put the property in an invalid state. Use one of the static factory + * methods instead. + * @param value the value (e.g. "AUDIO") + */ + public Action(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Action(Action original) { + super(original); + } + + /** + * Creates an "audio" action property. + * @return the property + */ + public static Action audio() { + return create(AUDIO); + } + + /** + * Determines if this property is an "audio" action. + * @return true if it's an "audio" action, false if not + */ + public boolean isAudio() { + return is(AUDIO); + } + + /** + * Creates an "display" action property. + * @return the property + */ + public static Action display() { + return create(DISPLAY); + } + + /** + * Determines if this property is an "display" action. + * @return true if it's an "display" action, false if not + */ + public boolean isDisplay() { + return is(DISPLAY); + } + + /** + * Creates an "email" action property. + * @return the property + */ + public static Action email() { + return create(EMAIL); + } + + /** + * Determines if this property is an "email" action. + * @return true if it's an "email" action, false if not + */ + public boolean isEmail() { + return is(EMAIL); + } + + /** + * Creates a "procedure" action property (vCal 1.0 only). + * @return the property + */ + public static Action procedure() { + return create(PROCEDURE); + } + + /** + * Determines if this property is a "procedure" action (vCal 1.0 only). + * @return true if it's a "procedure" action, false if not + */ + public boolean isProcedure() { + return is(PROCEDURE); + } + + private static Action create(String value) { + return new Action(value); + } + + @Override + protected Collection getStandardValues(ICalVersion version) { + switch (version) { + case V1_0: + return Arrays.asList(AUDIO, DISPLAY, EMAIL, PROCEDURE); + default: + return Arrays.asList(AUDIO, DISPLAY, EMAIL); + } + } + + @Override + protected Collection getValueSupportedVersions() { + if (value == null) { + return Collections.emptyList(); + } + + if (isAudio() || isDisplay() || isEmail()) { + return Arrays.asList(ICalVersion.values()); + } + if (isProcedure()) { + return Collections.singletonList(ICalVersion.V1_0); + } + + return Collections.emptyList(); + } + + @Override + public Action copy() { + return new Action(this); + } +} diff --git a/app/src/main/java/biweekly/property/Attachment.java b/app/src/main/java/biweekly/property/Attachment.java new file mode 100644 index 0000000000..6fd106a56f --- /dev/null +++ b/app/src/main/java/biweekly/property/Attachment.java @@ -0,0 +1,180 @@ +package biweekly.property; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines an file attachment (such as an image or document) that is associated + * with the component to which it belongs. + *

+ * + *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //from a byte array
+ * byte[] data = ...
+ * Attachment attach = new Attachment("image/png", data);
+ * event.addAttachment(attach);
+ * 
+ * //from a file 
+ * File file = new File("image.png");
+ * attach = new Attachment("image/png", file);
+ * event.addAttachment(attach);
+ * 
+ * //referencing a URL
+ * attach = new Attachment("image/png", "http://example.com/image.png");
+ * event.addAttachment(attach);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.80-1 + * @see RFC 2445 p.77-8 + * @see vCal 1.0 p.25 + */ +public class Attachment extends BinaryProperty { + private String contentId; + + /** + * Creates a new attachment. + * @param formatType the content-type of the data (e.g. "image/png") + * @param file the file to attach + * @throws IOException if there's a problem reading from the file + */ + public Attachment(String formatType, File file) throws IOException { + super(file); + setFormatType(formatType); + } + + /** + * Creates a new attachment. + * @param formatType the content-type of the data (e.g. "image/png") + * @param data the binary data + */ + public Attachment(String formatType, byte[] data) { + super(data); + setFormatType(formatType); + } + + /** + * Creates a new attachment. + * @param formatType the content-type of the data (e.g. "image/png") + * @param uri a URL pointing to the resource (e.g. + * "http://example.com/image.png") + */ + public Attachment(String formatType, String uri) { + super(uri); + setFormatType(formatType); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Attachment(Attachment original) { + super(original); + contentId = original.contentId; + } + + @Override + public void setData(byte[] data) { + super.setData(data); + contentId = null; + } + + @Override + public void setUri(String uri) { + super.setUri(uri); + contentId = null; + } + + /** + * Sets the content ID. + * @return the content ID or null if not set + */ + public String getContentId() { + return contentId; + } + + /** + * Sets the content ID. + * @param contentId the content ID + */ + public void setContentId(String contentId) { + this.contentId = contentId; + uri = null; + data = null; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (uri == null && data == null && contentId == null) { + warnings.add(new ValidationWarning(26)); + } + } + + @Override + protected Map toStringValues() { + Map values = super.toStringValues(); + values.put("contentId", contentId); + return values; + } + + @Override + public Attachment copy() { + return new Attachment(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((contentId == null) ? 0 : contentId.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Attachment other = (Attachment) obj; + if (contentId == null) { + if (other.contentId != null) return false; + } else if (!contentId.equals(other.contentId)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Attendee.java b/app/src/main/java/biweekly/property/Attendee.java new file mode 100644 index 0000000000..6459052c2f --- /dev/null +++ b/app/src/main/java/biweekly/property/Attendee.java @@ -0,0 +1,422 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.component.VAlarm; +import biweekly.parameter.CalendarUserType; +import biweekly.parameter.ParticipationLevel; +import biweekly.parameter.ParticipationStatus; +import biweekly.parameter.Role; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines an attendee (such as a person attending an event). This property has + * different meanings depending on the component that it belongs to: + *

+ *
    + *
  • {@link VAlarm} (with "EMAIL" action) - An email address that is to + * receive the alarm.
  • + *
  • All others - An attendee of the calendar entity.
  • + *
+ * + *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Attendee attendee = Attendee.email("johndoe@example.com")
+ * attendee.setCommonName("John Doe");
+ * attendee.setRsvp(true);
+ * attendee.setRole(Role.CHAIR);
+ * attendee.setParticipationStatus(ParticipationStatus.ACCEPTED);
+ * event.addAttendee(attendee);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.107-9 + * @see RFC 2445 + * p.102-4 + * @see vCal 1.0 p.25-7 + */ +public class Attendee extends ICalProperty { + private String name, email, uri; + private Role role; + private ParticipationLevel participationLevel; + private ParticipationStatus status; + private Boolean rsvp; + + /** + * Creates an attendee property. + * @param name the attendee's name (e.g. "John Doe") + * @param email the attendee's email (e.g. "jdoe@example.com") + */ + public Attendee(String name, String email) { + this(name, email, null); + } + + /** + * Creates an attendee property. + * @param name the attendee's name (e.g. "John Doe") + * @param email the attendee's email (e.g. "jdoe@example.com") + * @param uri a URI representing the attendee + */ + public Attendee(String name, String email, String uri) { + this.name = name; + this.email = email; + this.uri = uri; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Attendee(Attendee original) { + super(original); + name = original.name; + email = original.email; + uri = original.uri; + role = original.role; + participationLevel = original.participationLevel; + status = original.status; + rsvp = original.rsvp; + } + + /** + * Gets the attendee's email + * @return the email (e.g. "jdoe@company.com") + */ + public String getEmail() { + return email; + } + + /** + * Sets the attendee's email + * @param email the email (e.g. "jdoe@company.com") + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Gets a URI representing the attendee. + * @return the URI (e.g. "mailto:jdoe@company.com") + */ + public String getUri() { + return uri; + } + + /** + * Sets a URI representing the attendee. + * @param uri the URI (e.g. "mailto:jdoe@company.com") + */ + public void setUri(String uri) { + this.uri = uri; + } + + /** + *

+ * Gets the type of user the attendee is (for example, an "individual" or a + * "room"). + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return the calendar user type or null if not set + * @see RFC 5545 + * p.16 + */ + public CalendarUserType getCalendarUserType() { + return parameters.getCalendarUserType(); + } + + /** + *

+ * Sets the type of user the attendee is (for example, an "individual" or a + * "room"). + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @param cutype the calendar user type or null to remove + * @see RFC 5545 + * p.16 + */ + public void setCalendarUserType(CalendarUserType cutype) { + parameters.setCalendarUserType(cutype); + } + + /** + *

+ * Gets the list that holds the groups that the attendee is a member of. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return the groups (this list is mutable). Typically, these are email + * address URIs (e.g. "mailto:mailinglist@example.com") + * @see RFC 5545 + * p.21-2 + */ + public List getMemberOf() { + return parameters.getMembers(); + } + + /** + * Gets an attendee's role (for example, "chair" or "attendee"). + * @return the role or null if not set + * @see RFC 5545 + * p.25-6 + * @see vCal 1.0 p.25 + */ + public Role getRole() { + return role; + } + + /** + * Sets an attendee's role (for example, "chair" or "attendee"). + * @param role the role or null to remove + * @see RFC 5545 + * p.25-6 + * @see vCal 1.0 p.25 + */ + public void setRole(Role role) { + this.role = role; + } + + /** + * Gets an attendee's level of participation. + * @return the participation level or null if not set + * @see RFC 5545 + * p.25-6 + * @see vCal 1.0 p.26-7 + */ + public ParticipationLevel getParticipationLevel() { + return participationLevel; + } + + /** + * Sets an attendee's level of participation. + * @param level the participation level or null to remove + * @see RFC 5545 + * p.25-6 + * @see vCal 1.0 p.26-7 + */ + public void setParticipationLevel(ParticipationLevel level) { + this.participationLevel = level; + } + + /** + * Gets an attendee's participation status + * @return the participation status or null if not set + * @see RFC 5545 + * p.22-3 + * @see vCal 1.0 p.25-6 + */ + public ParticipationStatus getParticipationStatus() { + return status; + } + + /** + * Sets an attendee's participation status. + * @param status the participation status or null to remove + * @see RFC 5545 + * p.22-3 + * @see vCal 1.0 p.25-6 + */ + public void setParticipationStatus(ParticipationStatus status) { + this.status = status; + } + + /** + * Gets whether the organizer requests a response from the attendee. + * @return true if an RSVP is requested, false if not, null if not set + * @see RFC 5545 + * p.26-7 + */ + public Boolean getRsvp() { + return rsvp; + } + + /** + * Sets whether the organizer requests a response from the attendee. + * @param rsvp true if an RSVP has been requested, false if not, null to + * remove + * @see RFC 5545 + * p.26-7 + */ + public void setRsvp(Boolean rsvp) { + this.rsvp = rsvp; + } + + /** + *

+ * Gets the list containing the people who have delegated their + * responsibility to the attendee. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return the delegators (this list is mutable). Typically, these are email + * URIs (e.g. "mailto:janedoe@example.com"). + * @see RFC 5545 + * p.17 + */ + public List getDelegatedFrom() { + return parameters.getDelegatedFrom(); + } + + /** + *

+ * Gets the list containing the people to which the attendee has delegated + * his or her responsibility. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return the delegatees (this list is mutable). Typically, these are email + * URIs (e.g. "mailto:janedoe@example.com"). + * @see RFC 5545 + * p.17-8 + */ + public List getDelegatedTo() { + return parameters.getDelegatedTo(); + } + + @Override + public String getSentBy() { + return super.getSentBy(); + } + + @Override + public void setSentBy(String uri) { + super.setSentBy(uri); + } + + @Override + public String getCommonName() { + return name; + } + + @Override + public void setCommonName(String commonName) { + this.name = commonName; + } + + @Override + public String getDirectoryEntry() { + return super.getDirectoryEntry(); + } + + @Override + public void setDirectoryEntry(String uri) { + super.setDirectoryEntry(uri); + } + + /** + * Gets the language that the common name is written in. + */ + @Override + public String getLanguage() { + return super.getLanguage(); + } + + /** + * Sets the language that the common name is written in. + */ + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + //TODO + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("name", name); + values.put("email", email); + values.put("uri", uri); + values.put("role", role); + values.put("participationLevel", participationLevel); + values.put("status", status); + values.put("rsvp", rsvp); + return values; + } + + @Override + public Attendee copy() { + return new Attendee(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((email == null) ? 0 : email.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((participationLevel == null) ? 0 : participationLevel.hashCode()); + result = prime * result + ((role == null) ? 0 : role.hashCode()); + result = prime * result + ((rsvp == null) ? 0 : rsvp.hashCode()); + result = prime * result + ((status == null) ? 0 : status.hashCode()); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Attendee other = (Attendee) obj; + if (email == null) { + if (other.email != null) return false; + } else if (!email.equals(other.email)) return false; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + if (participationLevel != other.participationLevel) return false; + if (role != other.role) return false; + if (rsvp == null) { + if (other.rsvp != null) return false; + } else if (!rsvp.equals(other.rsvp)) return false; + if (status != other.status) return false; + if (uri == null) { + if (other.uri != null) return false; + } else if (!uri.equals(other.uri)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/AudioAlarm.java b/app/src/main/java/biweekly/property/AudioAlarm.java new file mode 100644 index 0000000000..ec062665ab --- /dev/null +++ b/app/src/main/java/biweekly/property/AudioAlarm.java @@ -0,0 +1,135 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Map; + +import biweekly.component.VAlarm; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines an alarm that will play an audio file when triggered. It is + * recommended that the {@link VAlarm} component be used to create alarms. + * @author Michael Angstadt + * @see vCal 1.0 p.27-8 + * @see VAlarm#audio + */ +public class AudioAlarm extends VCalAlarmProperty { + private String contentId, uri; + private byte[] data; + + public AudioAlarm() { + //empty + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public AudioAlarm(AudioAlarm original) { + super(original); + data = (original.data == null) ? null : original.data.clone(); + uri = original.uri; + contentId = original.contentId; + } + + public String getContentId() { + return contentId; + } + + public void setContentId(String contentId) { + this.contentId = contentId; + this.uri = null; + this.data = null; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + this.contentId = null; + this.data = null; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + this.uri = null; + this.contentId = null; + } + + public String getType() { + return parameters.getType(); + } + + public void setType(String type) { + parameters.setType(type); + } + + @Override + protected Map toStringValues() { + Map values = super.toStringValues(); + values.put("data", (data == null) ? "null" : "length: " + data.length); + values.put("uri", uri); + values.put("contentId", contentId); + return values; + } + + @Override + public AudioAlarm copy() { + return new AudioAlarm(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((contentId == null) ? 0 : contentId.hashCode()); + result = prime * result + Arrays.hashCode(data); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + AudioAlarm other = (AudioAlarm) obj; + if (contentId == null) { + if (other.contentId != null) return false; + } else if (!contentId.equals(other.contentId)) return false; + if (uri == null) { + if (other.uri != null) return false; + } else if (!uri.equals(other.uri)) return false; + if (!Arrays.equals(data, other.data)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/BinaryProperty.java b/app/src/main/java/biweekly/property/BinaryProperty.java new file mode 100644 index 0000000000..07ed72635b --- /dev/null +++ b/app/src/main/java/biweekly/property/BinaryProperty.java @@ -0,0 +1,163 @@ +package biweekly.property; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.Gobble; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * A property whose value is a binary resource (such as an image or document). + * @author Michael Angstadt + */ +public class BinaryProperty extends ICalProperty { + protected byte[] data; + protected String uri; + + /** + * Creates a new binary property. + * @param file a file containing the binary data + * @throws IOException if there's a problem reading from the file + */ + public BinaryProperty(File file) throws IOException { + this.data = new Gobble(file).asByteArray(); + } + + /** + * Creates a new binary property. + * @param data the binary data + */ + public BinaryProperty(byte[] data) { + this.data = data; + } + + /** + * Creates a new binary property. + * @param uri a URL pointing to the resource (e.g. + * "http://example.com/image.png") + */ + public BinaryProperty(String uri) { + this.uri = uri; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public BinaryProperty(BinaryProperty original) { + super(original); + data = (original.data == null) ? null : original.data.clone(); + uri = original.uri; + } + + /** + * Gets the property's binary data. + * @return the binary data or null if not set + */ + public byte[] getData() { + return data; + } + + /** + * Sets the property's binary data. + * @param data the binary data + */ + public void setData(byte[] data) { + this.data = data; + uri = null; + } + + /** + * Gets the property's URI. + * @return the URI (e.g. "http://example.com/image.png") or null if not set + */ + public String getUri() { + return uri; + } + + /** + * Sets the property's URI. + * @param uri the URI (e.g. "http://example.com/image.png") + */ + public void setUri(String uri) { + this.uri = uri; + data = null; + } + + @Override + public String getFormatType() { + return super.getFormatType(); + } + + @Override + public void setFormatType(String formatType) { + super.setFormatType(formatType); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (uri == null && data == null) { + warnings.add(new ValidationWarning(26)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("data", (data == null) ? "null" : "length: " + data.length); + values.put("uri", uri); + return values; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Arrays.hashCode(data); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + BinaryProperty other = (BinaryProperty) obj; + if (uri == null) { + if (other.uri != null) return false; + } else if (!uri.equals(other.uri)) return false; + if (!Arrays.equals(data, other.data)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/CalendarScale.java b/app/src/main/java/biweekly/property/CalendarScale.java new file mode 100644 index 0000000000..e738915214 --- /dev/null +++ b/app/src/main/java/biweekly/property/CalendarScale.java @@ -0,0 +1,126 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the calendar system that this iCalendar object uses for all its date + * values. If none is specified, then the calendar is assumed to be in + * "gregorian" format. + *

+ * + *

+ * Code sample (creating): + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * ical.setCalendarScale(CalendarScale.gregorian());
+ * 
+ * ical = new ICalendar();
+ * ical.setCalendarScale(new CalendarScale("another-calendar-system"));
+ * 
+ * + *

+ * Code sample (retrieving): + *

+ * + *
+ * ICalendar ical = ...
+ * CalendarScale calscale = ical.getCalendarscale();
+ * 
+ * if (calscale.isGregorian()) {
+ *   //...
+ * } else {
+ *   String value = calscale.getValue();
+ *   //...
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.76-7 + * @see RFC 2445 p.73-4 + */ +public class CalendarScale extends EnumProperty { + public static final String GREGORIAN = "GREGORIAN"; + + /** + * Creates a new calendar scale property. Use of this constructor is + * discouraged and may put the property in an invalid state. Use one of the + * static factory methods instead. + * @param value the value of the property (e.g. "gregorian") + */ + public CalendarScale(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public CalendarScale(CalendarScale original) { + super(original); + } + + /** + * Creates a new property whose value is set to "gregorian". + * @return the new property + */ + public static CalendarScale gregorian() { + return new CalendarScale(GREGORIAN); + } + + /** + * Determines whether the property is set to "gregorian". + * @return true if it's set to "gregorian", false if not + */ + public boolean isGregorian() { + return is(GREGORIAN); + } + + @Override + protected Collection getStandardValues(ICalVersion version) { + return Collections.singletonList(GREGORIAN); + } + + @Override + protected Collection getValueSupportedVersions() { + if (value == null) { + return Collections.emptyList(); + } + return Arrays.asList(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } + + @Override + public CalendarScale copy() { + return new CalendarScale(this); + } +} diff --git a/app/src/main/java/biweekly/property/Categories.java b/app/src/main/java/biweekly/property/Categories.java new file mode 100644 index 0000000000..17ff25845a --- /dev/null +++ b/app/src/main/java/biweekly/property/Categories.java @@ -0,0 +1,98 @@ +package biweekly.property; + +import java.util.List; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a list of keywords that describe the component to which it belongs. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Categories categories = new Categories("conference", "meeting");
+ * event.addCategories(categories);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.81-2 + * @see RFC 2445 p.78-9 + * @see vCal 1.0 p.28 + * @see draft-ietf-calext-extensions-01 + * p.7 + */ +public class Categories extends ListProperty { + /** + * Creates a new categories property. + */ + public Categories() { + super(); + } + + /** + * Creates a new categories property. + * @param categories the categories to initialize the property with + */ + public Categories(String... categories) { + super(categories); + } + + /** + * Creates a new categories property. + * @param categories the categories to initialize the property with + */ + public Categories(List categories) { + super(categories); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Categories(Categories original) { + super(original); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Categories copy() { + return new Categories(this); + } +} diff --git a/app/src/main/java/biweekly/property/Classification.java b/app/src/main/java/biweekly/property/Classification.java new file mode 100644 index 0000000000..1ba73b061f --- /dev/null +++ b/app/src/main/java/biweekly/property/Classification.java @@ -0,0 +1,153 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the level of sensitivity of the iCalendar data. If not specified, the + * data should be considered "public". + *

+ *

+ * Code sample (creating): + *

+ * + *
+ * VEvent event = new VEvent();
+ * event.setClassification(Classification.public_());
+ * 
+ * + *

+ * Code sample (retrieving): + *

+ * + *
+ * ICalendar ical = ...
+ * VEvent event = ical.getEvents().get(0);
+ * 
+ * Classification classification = event.getClassification();
+ * if (classification.isPublic()) {
+ *   //...
+ * } else if (classification.isPrivate()) {
+ *   //...
+ * } else if (classification.isConfidential()) {
+ *   //...
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.82-3 + * @see RFC 2445 + * p.79-80 + * @see vCal 1.0 p.28-9 + */ +public class Classification extends EnumProperty { + public static final String PUBLIC = "PUBLIC"; + public static final String PRIVATE = "PRIVATE"; + public static final String CONFIDENTIAL = "CONFIDENTIAL"; + + /** + * Creates a new classification property. Use the static factory methods to + * create a property with a standard classification level. + * @param classification the classification level (e.g. "PUBLIC") + */ + public Classification(String classification) { + super(classification); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Classification(Classification original) { + super(original); + } + + /** + * Creates a "public" classification property. + * @return the property + */ + public static Classification public_() { + return create(PUBLIC); + } + + /** + * Determines if the classification level is "public". + * @return true if it's "public", false if not + */ + public boolean isPublic() { + return is(PUBLIC); + } + + /** + * Creates a "private" classification property. + * @return the property + */ + public static Classification private_() { + return create(PRIVATE); + } + + /** + * Determines if the classification level is "private". + * @return true if it's "private", false if not + */ + public boolean isPrivate() { + return is(PRIVATE); + } + + /** + * Creates a "confidential" classification property. + * @return the property + */ + public static Classification confidential() { + return create(CONFIDENTIAL); + } + + /** + * Determines if the classification level is "confidential". + * @return true if it's "confidential", false if not + */ + public boolean isConfidential() { + return is(CONFIDENTIAL); + } + + private static Classification create(String classification) { + return new Classification(classification); + } + + @Override + protected Collection getStandardValues(ICalVersion version) { + return Arrays.asList(PUBLIC, PRIVATE, CONFIDENTIAL); + } + + @Override + public Classification copy() { + return new Classification(this); + } +} diff --git a/app/src/main/java/biweekly/property/Color.java b/app/src/main/java/biweekly/property/Color.java new file mode 100644 index 0000000000..cdd1c8f3ce --- /dev/null +++ b/app/src/main/java/biweekly/property/Color.java @@ -0,0 +1,115 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a color that clients may use when displaying the component data. + * Clients may use this color in any way they wish. For example, they can use it + * as a background color. + *

+ *

+ * Acceptable values are defined in Section + * 4.3 of the CSS Color Module Level 3 Recommendation. For example, + * "aliceblue", "green", "navy". + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Color color = new Color("mistyrose");
+ * event.setColor(color);
+ * 
+ * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.9 + */ +public class Color extends TextProperty { + /** + * Creates a color property. + * @param color the color name (case insensitive). Acceptable values are + * defined in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + */ + public Color(String color) { + super(color); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Color(Color original) { + super(original); + } + + /** + * Gets the value of this property. + * @return the value (case insensitive). Acceptable values are defined in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + */ + @Override + public String getValue() { + return super.getValue(); + } + + /** + * Sets the value of this property. + * @param value the value (case insensitive). Acceptable values are defined + * in Section 4.3 of the CSS Color Module Level 3 Recommendation. For + * example, "aliceblue", "green", "navy". + */ + @Override + public void setValue(String value) { + super.setValue(value); + } + + @Override + public Color copy() { + return new Color(this); + } + + @Override + protected int valueHashCode() { + return value.toLowerCase().hashCode(); + } + + @Override + protected boolean valueEquals(String otherValue) { + return value.equalsIgnoreCase(otherValue); + } +} diff --git a/app/src/main/java/biweekly/property/Comment.java b/app/src/main/java/biweekly/property/Comment.java new file mode 100644 index 0000000000..b0c70919b3 --- /dev/null +++ b/app/src/main/java/biweekly/property/Comment.java @@ -0,0 +1,87 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a free-text comment. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Comment comment = new Comment("Free text");
+ * event.addComment(comment);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.83-4 + * @see RFC 2445 p.80-1 + */ +public class Comment extends TextProperty { + /** + * Creates a comment property. + * @param comment the comment + */ + public Comment(String comment) { + super(comment); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Comment(Comment original) { + super(original); + } + + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Comment copy() { + return new Comment(this); + } +} diff --git a/app/src/main/java/biweekly/property/Completed.java b/app/src/main/java/biweekly/property/Completed.java new file mode 100644 index 0000000000..c6033227ed --- /dev/null +++ b/app/src/main/java/biweekly/property/Completed.java @@ -0,0 +1,71 @@ +package biweekly.property; + +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the date and time that a to-do task was completed. + *

+ *

+ * Code sample: + *

+ * + *
+ * VTodo todo = new VTodo();
+ * 
+ * Date datetime = ...
+ * Completed completed = new Completed(datetime);
+ * todo.setCompleted(completed);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.94-5 + * @see RFC 2445 p.90-1 + * @see vCal 1.0 p.29 + */ +public class Completed extends DateTimeProperty { + /** + * Creates a completed property. + * @param completed the completion date + */ + public Completed(Date completed) { + super(completed); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Completed(Completed original) { + super(original); + } + + @Override + public Completed copy() { + return new Completed(this); + } +} diff --git a/app/src/main/java/biweekly/property/Conference.java b/app/src/main/java/biweekly/property/Conference.java new file mode 100644 index 0000000000..ef833939eb --- /dev/null +++ b/app/src/main/java/biweekly/property/Conference.java @@ -0,0 +1,132 @@ +package biweekly.property; + +import java.util.List; + +import biweekly.parameter.Feature; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Contains information used for accessing a conference system. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Conference conference = new Conference("tel:+1-412-555-0123,,,654321");
+ * conference.setLabel("Audio conference, access code=77869");
+ * conference.getFeatures().add(Feature.AUDIO);
+ * conference.getFeatures().add(Feature.PHONE);
+ * event.addConference(conference);
+ * 
+ * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.11 + */ +public class Conference extends ICalProperty { + private String uri, text; + + /** + * Creates a conference property. + * @param uri the uri + */ + public Conference(String uri) { + this.uri = uri; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Conference(Conference original) { + super(original); + uri = original.uri; + text = original.text; + } + + /** + * Gets the URI value of this property. + * @return the URI value or null if not set + */ + public String getUri() { + return uri; + } + + /** + * Sets the value of this property to a URI. + * @param uri the URI + */ + public void setUri(String uri) { + this.uri = uri; + text = null; + } + + /** + * Gets the plain text value of this property. + * @return the plain text value or null if not set + */ + public String getText() { + return text; + } + + /** + * Sets the value of this property to a plain text value. + * @param text the plain text value + */ + public void setText(String text) { + this.text = text; + uri = null; + } + + /** + * Gets the list that holds the features that this conference supports (for + * example, audio and video). + * @return the features (this list is mutable) + */ + public List getFeatures() { + return parameters.getFeatures(); + } + + @Override + public String getLabel() { + return super.getLabel(); + } + + @Override + public void setLabel(String label) { + super.setLabel(label); + } + + @Override + public Conference copy() { + return new Conference(this); + } +} diff --git a/app/src/main/java/biweekly/property/Contact.java b/app/src/main/java/biweekly/property/Contact.java new file mode 100644 index 0000000000..5b09611455 --- /dev/null +++ b/app/src/main/java/biweekly/property/Contact.java @@ -0,0 +1,96 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the contact information for a person or other entity (for example, + * the name of a business and its phone number). + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Contact contact = new Contact("Acme Co: (212) 555-1234");
+ * event.addContact(contact);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.109-11 + * @see RFC 2445 + * p.104-6 + */ +public class Contact extends TextProperty { + /** + * Creates a contact property. + * @param contact the contact information (e.g. "Acme Co: (212) 555-1234") + */ + public Contact(String contact) { + super(contact); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Contact(Contact original) { + super(original); + } + + /** + * @return the URI (such as a URL to a vCard) or null if not set + */ + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + /** + * @param uri the URI (such as a URL to a vCard) or null to remove + */ + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Contact copy() { + return new Contact(this); + } +} diff --git a/app/src/main/java/biweekly/property/Created.java b/app/src/main/java/biweekly/property/Created.java new file mode 100644 index 0000000000..72b9029e4f --- /dev/null +++ b/app/src/main/java/biweekly/property/Created.java @@ -0,0 +1,72 @@ +package biweekly.property; + +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the time that the calendar information was initially created. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Date datetime = ...
+ * Created created = new Created(datetime);
+ * event.setCreated(created);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.136 + * @see RFC 2445 + * p.129-30 + * @see vCal 1.0 p.29 + */ +public class Created extends DateTimeProperty { + /** + * Creates a created property. + * @param date the creation date + */ + public Created(Date date) { + super(date); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Created(Created original) { + super(original); + } + + @Override + public Created copy() { + return new Created(this); + } +} diff --git a/app/src/main/java/biweekly/property/DateDue.java b/app/src/main/java/biweekly/property/DateDue.java new file mode 100644 index 0000000000..587d2694e4 --- /dev/null +++ b/app/src/main/java/biweekly/property/DateDue.java @@ -0,0 +1,149 @@ +package biweekly.property; + +import java.util.Date; + +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the due date of a to-do task. + *

+ *

+ * Code sample (creating): + *

+ * + *
+ * VTodo todo = new VTodo();
+ * 
+ * //date and time
+ * Date datetime = ...
+ * DateDue due = new DateDue(datetime);
+ * todo.setDateDue(due);
+ * 
+ * //date (without time component)
+ * Date date = ...
+ * due = new DateDue(date, false);
+ * todo.setDateDue(due);
+ * 
+ * + *

+ * Code sample (reading): + *

+ * + *
+ * ICalendar ical = ...
+ * TimezoneInfo tzinfo = ical.getTimezoneInfo();
+ * 
+ * for (VTodo todo : ical.getTodos()) {
+ *   DateDue due = todo.getDateDue();
+ *   
+ *   //get property value (ICalDate extends java.util.Date)
+ *   ICalDate value = due.getValue();
+ *   
+ *   if (value.hasTime()) {
+ *     //the value includes a time component
+ *   } else {
+ *     //the value is just a date
+ *     //date object's time is set to "00:00:00" under local computer's default timezone
+ *   }
+ *   
+ *   //gets the timezone that the property value was parsed under if you are curious about that
+ *   TimeZone tz = tzinfo.getTimeZone(due);
+ * }
+ * 
+ * + *

+ * Code sample (using timezones): + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * VTodo todo = new VTodo();
+ * Date datetime = ...
+ * DateDue due = new DateDue(datetime);
+ * todo.setDateDue(due);
+ * ical.addTodo(todo);
+ * 
+ * TimezoneAssignment tz = ...
+ * 
+ * //set the timezone of all date-time property values
+ * //date-time property values are written in UTC by default
+ * ical.getTimezoneInfo().setDefaultTimezone(tz);
+ * 
+ * //or set the timezone just for this property
+ * ical.getTimezoneInfo().setTimezone(due, tz);
+ * 
+ * //finally, write the iCalendar object
+ * ICalWriter writer = ...
+ * writer.write(ical);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.96-7 + * @see RFC 2445 p.92-3 + * @see vCal 1.0 p.30 + */ +public class DateDue extends DateOrDateTimeProperty { + /** + * Creates a due date property. + * @param dueDate the due date + */ + public DateDue(Date dueDate) { + super(dueDate); + } + + /** + * Creates a due date property. + * @param dueDate the due date + * @param hasTime true if the value has a time component, false if it is + * strictly a date + */ + public DateDue(Date dueDate, boolean hasTime) { + super(dueDate, hasTime); + } + + /** + * Creates a due date property. + * @param dueDate the due date + */ + public DateDue(ICalDate dueDate) { + super(dueDate); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DateDue(DateDue original) { + super(original); + } + + @Override + public DateDue copy() { + return new DateDue(this); + } +} diff --git a/app/src/main/java/biweekly/property/DateEnd.java b/app/src/main/java/biweekly/property/DateEnd.java new file mode 100644 index 0000000000..01f0ff41e0 --- /dev/null +++ b/app/src/main/java/biweekly/property/DateEnd.java @@ -0,0 +1,147 @@ +package biweekly.property; + +import java.util.Date; + +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the end date of an event or free/busy component. + *

+ *

+ * Code sample (creating): + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //date and time
+ * Date datetime = ...
+ * DateEnd dtend = new DateEnd(datetime);
+ * event.setDateEnd(dtend);
+ * 
+ * //date (without time component)
+ * Date date = ...
+ * dtend = new DateEnd(date, false);
+ * event.setDateEnd(dtend);
+ * 
+ * + *

+ * Code sample (reading): + *

+ * + *
+ * ICalendar ical = ...
+ * for (VEvent event : ical.getEvents()) {
+ *   DateEnd dtend = event.getDateEnd();
+ *   
+ *   //get property value (ICalDate extends java.util.Date)
+ *   ICalDate value = dtend.getValue();
+ *   
+ *   if (value.hasTime()) {
+ *     //the value includes a time component
+ *   } else {
+ *     //the value is just a date
+ *     //date object's time is set to "00:00:00" under local computer's default timezone
+ *   }
+ *   
+ *   //gets the timezone that the property value was parsed under if you are curious about that
+ *   TimeZone tz = tzinfo.getTimeZone(dtend);
+ * }
+ * 
+ * + *

+ * Code sample (using timezones): + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * VEvent event = new VEvent();
+ * Date datetime = ...
+ * DateEnd dtend = new DateEnd(datetime);
+ * event.setDateEnd(dtend);
+ * ical.addEvent(event);
+ * 
+ * TimezoneAssignment tz = ...
+ * 
+ * //set the timezone of all date-time property values
+ * //date-time property values are written in UTC by default
+ * ical.getTimezoneInfo().setDefaultTimezone(tz);
+ * 
+ * //or set the timezone just for this property
+ * ical.getTimezoneInfo().setTimezone(dtend, tz);
+ * 
+ * //finally, write the iCalendar object
+ * ICalWriter writer = ...
+ * writer.write(ical);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.95-6 + * @see RFC 2445 p.91-2 + * @see vCal 1.0 p.31 + */ +public class DateEnd extends DateOrDateTimeProperty { + /** + * Creates an end date property. + * @param endDate the end date + */ + public DateEnd(Date endDate) { + super(endDate); + } + + /** + * Creates an end date property. + * @param endDate the end date + * @param hasTime true if the value has a time component, false if it is + * strictly a date + */ + public DateEnd(Date endDate, boolean hasTime) { + super(endDate, hasTime); + } + + /** + * Creates an end date property. + * @param dateEnd the end date + */ + public DateEnd(ICalDate dateEnd) { + super(dateEnd); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DateEnd(DateEnd original) { + super(original); + } + + @Override + public DateEnd copy() { + return new DateEnd(this); + } +} diff --git a/app/src/main/java/biweekly/property/DateOrDateTimeProperty.java b/app/src/main/java/biweekly/property/DateOrDateTimeProperty.java new file mode 100644 index 0000000000..87fdc04949 --- /dev/null +++ b/app/src/main/java/biweekly/property/DateOrDateTimeProperty.java @@ -0,0 +1,88 @@ +package biweekly.property; + +import java.util.Date; + +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is a date or a date-time. + * @author Michael Angstadt + */ +public class DateOrDateTimeProperty extends ValuedProperty { + /** + * Creates a new property. + * @param value the date-time value + */ + public DateOrDateTimeProperty(ICalDate value) { + super(value); + } + + /** + * Creates a new property. + * @param value the date-time value + */ + public DateOrDateTimeProperty(Date value) { + this(value, true); + } + + /** + * Creates a new property. + * @param value the date-time value + * @param hasTime true if the value has a time component, false if it is + * strictly a date + */ + public DateOrDateTimeProperty(Date value, boolean hasTime) { + this(createICalDate(value, hasTime)); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DateOrDateTimeProperty(DateOrDateTimeProperty original) { + super(original); + value = (original.value == null) ? null : new ICalDate(original.value); + } + + /** + * Sets the date-time value. + * @param value the date-time value + * @param hasTime true if the value has a time component, false if it is + * strictly a date + */ + public void setValue(Date value, boolean hasTime) { + setValue((value == null) ? null : new ICalDate(value, hasTime)); + } + + private static ICalDate createICalDate(Date value, boolean hasTime) { + if (value == null) { + return null; + } + return (value instanceof ICalDate) ? (ICalDate) value : new ICalDate(value, hasTime); + } +} diff --git a/app/src/main/java/biweekly/property/DateStart.java b/app/src/main/java/biweekly/property/DateStart.java new file mode 100644 index 0000000000..31307f1a02 --- /dev/null +++ b/app/src/main/java/biweekly/property/DateStart.java @@ -0,0 +1,148 @@ +package biweekly.property; + +import java.util.Date; + +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the start date of an event, free/busy component, or timezone + * component. + *

+ *

+ * Code sample (creating): + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //date and time
+ * Date datetime = ...
+ * DateStart dtstart = new DateStart(datetime);
+ * event.setDateStart(dtstart);
+ * 
+ * //date (without time component)
+ * Date date = ...
+ * dtstart = new DateStart(date, false);
+ * event.setDateStart(dtstart);
+ * 
+ * + *

+ * Code sample (reading): + *

+ * + *
+ * ICalendar ical = ...
+ * for (VEvent event : ical.getEvents()) {
+ *   DateStart dtstart = event.getDateStart();
+ *   
+ *   //get property value (ICalDate extends java.util.Date)
+ *   ICalDate value = dtstart.getValue();
+ *   
+ *   if (value.hasTime()) {
+ *     //the value includes a time component
+ *   } else {
+ *     //the value is just a date
+ *     //date object's time is set to "00:00:00" under local computer's default timezone
+ *   }
+ *   
+ *   //gets the timezone that the property value was parsed under if you are curious about that
+ *   TimeZone tz = tzinfo.getTimeZone(dtstart);
+ * }
+ * 
+ * + *

+ * Code sample (using timezones): + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * VEvent event = new VEvent();
+ * Date datetime = ...
+ * DateStart dtstart = new DateStart(datetime);
+ * event.setDateStart(dtstart);
+ * ical.addEvent(event);
+ * 
+ * TimezoneAssignment tz = ...
+ * 
+ * //set the timezone of all date-time property values
+ * //date-time property values are written in UTC by default
+ * ical.getTimezoneInfo().setDefaultTimezone(tz);
+ * 
+ * //or set the timezone just for this property
+ * ical.getTimezoneInfo().setTimezone(dtstart, tz);
+ * 
+ * //finally, write the iCalendar object
+ * ICalWriter writer = ...
+ * writer.write(ical);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.97-8 + * @see RFC 2445 p.93-4 + * @see vCal 1.0 p.35 + */ +public class DateStart extends DateOrDateTimeProperty { + /** + * Creates a start date property. + * @param startDate the start date + */ + public DateStart(Date startDate) { + super(startDate); + } + + /** + * Creates a start date property. + * @param startDate the start date + * @param hasTime true if the value has a time component, false if it is + * strictly a date + */ + public DateStart(Date startDate, boolean hasTime) { + super(startDate, hasTime); + } + + /** + * Creates a start date property. + * @param startDate the start date + */ + public DateStart(ICalDate startDate) { + super(startDate); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DateStart(DateStart original) { + super(original); + } + + @Override + public DateStart copy() { + return new DateStart(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/property/DateTimeProperty.java b/app/src/main/java/biweekly/property/DateTimeProperty.java new file mode 100644 index 0000000000..22d4fe6e60 --- /dev/null +++ b/app/src/main/java/biweekly/property/DateTimeProperty.java @@ -0,0 +1,52 @@ +package biweekly.property; + +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is a date-time value. These properties are + * always written in UTC time. + * @author Michael Angstadt + */ +public class DateTimeProperty extends ValuedProperty { + /** + * Creates a new property. + * @param value the date-time value + */ + public DateTimeProperty(Date value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DateTimeProperty(DateTimeProperty original) { + super(original); + value = new Date(original.value.getTime()); + } +} diff --git a/app/src/main/java/biweekly/property/DateTimeStamp.java b/app/src/main/java/biweekly/property/DateTimeStamp.java new file mode 100644 index 0000000000..341898f1e9 --- /dev/null +++ b/app/src/main/java/biweekly/property/DateTimeStamp.java @@ -0,0 +1,82 @@ +package biweekly.property; + +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * The meaning of this property varies depending on whether the iCalendar object + * has a {@link Method} property. + *

+ *
    + *
  • Has a {@link Method} property: Defines the creation date of the + * iCalendar object itself (not the creation date of the actual calendar + * data from the originating data store). Use the {@link Created} property to + * define the creation date of the actual calendar data.
  • + *
  • Does not have a {@link Method} property: Defines the date that the + * calendar data was last updated (the {@link LastModified} property also holds + * this information).
  • + *
+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Date datetime = ... 
+ * DateTimeStamp dtstamp = new DateTimeStamp(datetime);
+ * event.setDateTimeStamp(dtstamp);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.137-8 + * @see RFC 2445 + * p.130-1 + */ +public class DateTimeStamp extends DateTimeProperty { + /** + * Creates a date time stamp property. + * @param date the date + */ + public DateTimeStamp(Date date) { + super(date); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DateTimeStamp(DateTimeStamp original) { + super(original); + } + + @Override + public DateTimeStamp copy() { + return new DateTimeStamp(this); + } +} diff --git a/app/src/main/java/biweekly/property/Daylight.java b/app/src/main/java/biweekly/property/Daylight.java new file mode 100644 index 0000000000..2bd9314db4 --- /dev/null +++ b/app/src/main/java/biweekly/property/Daylight.java @@ -0,0 +1,247 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.ICalDate; +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents daylight savings time information. + * @author Michael Angstadt + * @see vCal 1.0 p.23 + */ +public class Daylight extends ICalProperty { + private boolean daylight; + private UtcOffset offset; + private ICalDate start, end; + private String standardName, daylightName; + + /** + * Creates a daylight savings property which states that the timezone does + * not observe daylight savings time. + */ + public Daylight() { + this.daylight = false; + } + + /** + * Creates a daylight savings property. + * @param daylight true if the timezone observes daylight savings time, + * false if not + * @param offset the UTC offset of daylight savings time + * @param start the start date of daylight savings time + * @param end the end date of daylight savings time + * @param standardName the timezone's name for standard time (e.g. "EST") + * @param daylightName the timezone's name for daylight savings time (e.g. + * "EDT") + */ + public Daylight(boolean daylight, UtcOffset offset, ICalDate start, ICalDate end, String standardName, String daylightName) { + this.daylight = daylight; + this.offset = offset; + this.start = start; + this.end = end; + this.standardName = standardName; + this.daylightName = daylightName; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Daylight(Daylight original) { + super(original); + daylight = original.daylight; + offset = original.offset; + start = (original.start == null) ? null : new ICalDate(original.start); + end = (original.end == null) ? null : new ICalDate(original.end); + standardName = original.standardName; + daylightName = original.daylightName; + } + + /** + * Gets whether this timezone observes daylight savings time. + * @return true if it observes daylight savings time, false if not + */ + public boolean isDaylight() { + return daylight; + } + + /** + * Sets whether this timezone observes daylight savings time. + * @param daylight true if it observes daylight savings time, false if not + */ + public void setDaylight(boolean daylight) { + this.daylight = daylight; + } + + /** + * Gets the UTC offset of daylight savings time. + * @return the UTC offset + */ + public UtcOffset getOffset() { + return offset; + } + + /** + * Sets the UTC offset of daylight savings time. + * @param offset the UTC offset + */ + public void setOffset(UtcOffset offset) { + this.offset = offset; + } + + /** + * Gets the start date of dayight savings time. + * @return the start date + */ + public ICalDate getStart() { + return start; + } + + /** + * Sets the start date of dayight savings time. + * @param start the start date + */ + public void setStart(ICalDate start) { + this.start = start; + } + + /** + * Gets the end date of daylight savings time. + * @return the end date + */ + public ICalDate getEnd() { + return end; + } + + /** + * Sets the end date of daylight savings time. + * @param end the end date + */ + public void setEnd(ICalDate end) { + this.end = end; + } + + /** + * Gets the name for standard time. + * @return the name (e.g. "EST") + */ + public String getStandardName() { + return standardName; + } + + /** + * Sets the name for standard time. + * @param name the name (e.g. "EST") + */ + public void setStandardName(String name) { + this.standardName = name; + } + + /** + * Gets the name of daylight savings time. + * @return the name (e.g. "EDT") + */ + public String getDaylightName() { + return daylightName; + } + + /** + * Sets the name of daylight savings time. + * @param name the name (e.g. "EDT") + */ + public void setDaylightName(String name) { + this.daylightName = name; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (daylight && (offset == null || start == null || end == null || standardName == null || daylightName == null)) { + warnings.add(new ValidationWarning(43)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("daylight", daylight); + values.put("offset", offset); + values.put("start", start); + values.put("end", end); + values.put("standardName", standardName); + values.put("daylightName", daylightName); + return values; + } + + @Override + public Daylight copy() { + return new Daylight(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + (daylight ? 1231 : 1237); + result = prime * result + ((daylightName == null) ? 0 : daylightName.hashCode()); + result = prime * result + ((end == null) ? 0 : end.hashCode()); + result = prime * result + ((offset == null) ? 0 : offset.hashCode()); + result = prime * result + ((standardName == null) ? 0 : standardName.hashCode()); + result = prime * result + ((start == null) ? 0 : start.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Daylight other = (Daylight) obj; + if (daylight != other.daylight) return false; + if (daylightName == null) { + if (other.daylightName != null) return false; + } else if (!daylightName.equals(other.daylightName)) return false; + if (end == null) { + if (other.end != null) return false; + } else if (!end.equals(other.end)) return false; + if (offset == null) { + if (other.offset != null) return false; + } else if (!offset.equals(other.offset)) return false; + if (standardName == null) { + if (other.standardName != null) return false; + } else if (!standardName.equals(other.standardName)) return false; + if (start == null) { + if (other.start != null) return false; + } else if (!start.equals(other.start)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Description.java b/app/src/main/java/biweekly/property/Description.java new file mode 100644 index 0000000000..8ca952c1f5 --- /dev/null +++ b/app/src/main/java/biweekly/property/Description.java @@ -0,0 +1,102 @@ +package biweekly.property; + +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a detailed description of the component that this property belongs + * to. The description should be a more detailed version of the text provided by + * the {@link Summary} property. + *

+ *

+ * If defined in the top-level {@link ICalendar} component, it contains a + * detailed description of the calendar as a whole. An iCalendar object can only + * have one description, but multiple Description properties can exist in order + * to specify the description in multiple languages. In this case, each property + * instance must be assigned a LANGUAGE parameter. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Description description = new Description("During this meeting, we will discuss...");
+ * event.setDescription(description);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.84-5 + * @see RFC 2445 p.81-2 + * @see vCal 1.0 p.30 + * @see draft-ietf-calext-extensions-01 + * p.6 + */ +public class Description extends TextProperty { + /** + * Creates a description property. + * @param description the description + */ + public Description(String description) { + super(description); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Description(Description original) { + super(original); + } + + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Description copy() { + return new Description(this); + } +} diff --git a/app/src/main/java/biweekly/property/DisplayAlarm.java b/app/src/main/java/biweekly/property/DisplayAlarm.java new file mode 100644 index 0000000000..07ee706e4d --- /dev/null +++ b/app/src/main/java/biweekly/property/DisplayAlarm.java @@ -0,0 +1,101 @@ +package biweekly.property; + +import java.util.Map; + +import biweekly.component.VAlarm; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines an alarm that displays a message when triggered. It is recommended + * that the {@link VAlarm} component be used to created alarms. + * @author Michael Angstadt + * @see vCal 1.0 p.30 + * @see VAlarm#display + */ +public class DisplayAlarm extends VCalAlarmProperty { + private String text; + + public DisplayAlarm(String text) { + this.text = text; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DisplayAlarm(DisplayAlarm original) { + super(original); + text = original.text; + } + + /** + * Gets the text to display when the alarm is triggered. + * @return the display text + */ + public String getText() { + return text; + } + + /** + * Sets the text to display when the alarm is triggered. + * @param text the display text + */ + public void setText(String text) { + this.text = text; + } + + @Override + protected Map toStringValues() { + Map values = super.toStringValues(); + values.put("text", text); + return values; + } + + @Override + public DisplayAlarm copy() { + return new DisplayAlarm(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((text == null) ? 0 : text.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + DisplayAlarm other = (DisplayAlarm) obj; + if (text == null) { + if (other.text != null) return false; + } else if (!text.equals(other.text)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/DurationProperty.java b/app/src/main/java/biweekly/property/DurationProperty.java new file mode 100644 index 0000000000..cbe7fd57ea --- /dev/null +++ b/app/src/main/java/biweekly/property/DurationProperty.java @@ -0,0 +1,81 @@ +package biweekly.property; + +import biweekly.component.VAlarm; +import biweekly.component.VEvent; +import biweekly.component.VTodo; +import biweekly.util.Duration; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a duration of time (for example, "2 hours and 30 minutes"). This + * property has different meanings depending on the component it belongs to: + *

+ *
    + *
  • {@link VEvent} - The duration of the event (used in place of a + * {@link DateEnd} property).
  • + *
  • {@link VTodo} - The duration of the to-do task (used in place of a + * {@link DateEnd} property).
  • + *
  • {@link VAlarm} - The pause between alarm repetitions.
  • + *
+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Duration duration = Duration.builder().hours(2).minutes(30).build();
+ * DurationProperty prop = new DurationProperty(duration);
+ * event.setDuration(prop);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.99 + * @see RFC 2445 p.94-5 + */ +public class DurationProperty extends ValuedProperty { + /** + * Creates a duration property. + * @param duration the duration value (e.g. "2 hours and 30 minutes") + */ + public DurationProperty(Duration duration) { + super(duration); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public DurationProperty(DurationProperty original) { + super(original); + } + + @Override + public DurationProperty copy() { + return new DurationProperty(this); + } +} diff --git a/app/src/main/java/biweekly/property/EmailAlarm.java b/app/src/main/java/biweekly/property/EmailAlarm.java new file mode 100644 index 0000000000..a790521384 --- /dev/null +++ b/app/src/main/java/biweekly/property/EmailAlarm.java @@ -0,0 +1,123 @@ +package biweekly.property; + +import java.util.Map; + +import biweekly.component.VAlarm; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines an alarm that sends an email when triggered. It is recommended that + * the {@link VAlarm} component be used to create alarms. + * @author Michael Angstadt + * @see vCal 1.0 p.32 + * @see VAlarm#email + */ +public class EmailAlarm extends VCalAlarmProperty { + private String email, note; + + public EmailAlarm(String email) { + this.email = email; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public EmailAlarm(EmailAlarm original) { + super(original); + email = original.email; + note = original.note; + } + + /** + * Gets the email address. + * @return the email address + */ + public String getEmail() { + return email; + } + + /** + * Sets the email address. + * @param email the email address + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Gets the note to send. + * @return the note + */ + public String getNote() { + return note; + } + + /** + * Sets the note to send + * @param note the note + */ + public void setNote(String note) { + this.note = note; + } + + @Override + protected Map toStringValues() { + Map values = super.toStringValues(); + values.put("email", email); + values.put("note", note); + return values; + } + + @Override + public EmailAlarm copy() { + return new EmailAlarm(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((email == null) ? 0 : email.hashCode()); + result = prime * result + ((note == null) ? 0 : note.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + EmailAlarm other = (EmailAlarm) obj; + if (email == null) { + if (other.email != null) return false; + } else if (!email.equals(other.email)) return false; + if (note == null) { + if (other.note != null) return false; + } else if (!note.equals(other.note)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/EnumProperty.java b/app/src/main/java/biweekly/property/EnumProperty.java new file mode 100644 index 0000000000..81b377e000 --- /dev/null +++ b/app/src/main/java/biweekly/property/EnumProperty.java @@ -0,0 +1,125 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property that has a defined set of acceptable values (for + * example, the {@link Action} property). + * @author Michael Angstadt + */ +public abstract class EnumProperty extends TextProperty { + /** + * Creates an enum property. + * @param value the property value + */ + public EnumProperty(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public EnumProperty(EnumProperty original) { + super(original); + } + + /** + * Compares the property's value with a given string (case-insensitive). + * @param value the string + * @return true if it's equal, false if not + */ + protected boolean is(String value) { + return value.equalsIgnoreCase(this.value); + } + + /** + * Gets the list of acceptable values for this property. + * @param version the version + * @return the list of acceptable values + */ + protected abstract Collection getStandardValues(ICalVersion version); + + /** + * Gets the iCalendar versions that this property's value is supported in. + * Meant to be overridden by the child class. + * @return the supported versions + */ + protected Collection getValueSupportedVersions() { + return (value == null) ? Collections. emptyList() : Arrays.asList(ICalVersion.values()); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + if (value == null) { + return; + } + + Collection supportedVersions = getValueSupportedVersions(); + if (supportedVersions.isEmpty()) { + //it's a non-standard value + warnings.add(new ValidationWarning(28, value, getStandardValues(version))); + return; + } + + boolean supported = supportedVersions.contains(version); + if (!supported) { + warnings.add(new ValidationWarning(46, value, supportedVersions)); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + parameters.hashCode(); + result = prime * result + ((value == null) ? 0 : value.toLowerCase().hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + EnumProperty other = (EnumProperty) obj; + if (!parameters.equals(other.parameters)) return false; + if (value == null) { + if (other.value != null) return false; + } else if (!value.equalsIgnoreCase(other.value)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/ExceptionDates.java b/app/src/main/java/biweekly/property/ExceptionDates.java new file mode 100644 index 0000000000..13c63b44f5 --- /dev/null +++ b/app/src/main/java/biweekly/property/ExceptionDates.java @@ -0,0 +1,106 @@ +package biweekly.property; + +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a list of exceptions to the dates specified in the + * {@link RecurrenceRule} property. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //dates with time components
+ * ExceptionDates exdate = new ExceptionDates();
+ * Date datetime = ...
+ * exdate.getValues().add(new ICalDate(datetime, true));
+ * event.addExceptionDates(exdate);
+ * 
+ * //dates without time components
+ * exdate = new ExceptionDates();
+ * Date date = ...
+ * exdate.getValues().add(new ICalDate(date, false));
+ * event.addExceptionDates(exdate);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.118-20 + * @see RFC 2445 + * p.112-4 + * @see vCal 1.0 p.31 + */ +public class ExceptionDates extends ListProperty { + public ExceptionDates() { + //empty + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public ExceptionDates(ExceptionDates original) { + super(original); + values.clear(); + for (ICalDate date : original.getValues()) { + values.add(new ICalDate(date)); + } + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + + List dates = getValues(); + if (dates.isEmpty()) { + return; + } + + //can't mix date and date-time values + boolean hasTime = dates.get(0).hasTime(); + for (ICalDate date : dates.subList(1, dates.size())) { + if (date.hasTime() != hasTime) { + warnings.add(new ValidationWarning(50)); + break; + } + } + } + + @Override + public ExceptionDates copy() { + return new ExceptionDates(this); + } +} diff --git a/app/src/main/java/biweekly/property/ExceptionRule.java b/app/src/main/java/biweekly/property/ExceptionRule.java new file mode 100644 index 0000000000..b94bbdea38 --- /dev/null +++ b/app/src/main/java/biweekly/property/ExceptionRule.java @@ -0,0 +1,91 @@ +package biweekly.property; + +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.Recurrence; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a list of exceptions to the dates specified in the + * {@link RecurrenceRule} property. + *

+ *

+ * Note that this property has been removed from the latest version of the iCal + * specification. Its use should be avoided. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //"bi-weekly"
+ * Recurrence recur = new Recurrence.Builder(Frequency.WEEKLY).interval(2).build();
+ * ExceptionRule exrule = new ExceptionRule(recur);
+ * event.addExceptionRule(exrule);
+ * 
+ * @author Michael Angstadt + * @see RFC 2445 + * p.114-15 + * @see vCal 1.0 p.31 + */ +public class ExceptionRule extends RecurrenceProperty { + /** + * Creates a new exception rule property. + * @param recur the recurrence rule + */ + public ExceptionRule(Recurrence recur) { + super(recur); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public ExceptionRule(ExceptionRule original) { + super(original); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + + if (version == ICalVersion.V2_0) { + warnings.add(new ValidationWarning(37)); + } + } + + @Override + public ExceptionRule copy() { + return new ExceptionRule(this); + } +} diff --git a/app/src/main/java/biweekly/property/FreeBusy.java b/app/src/main/java/biweekly/property/FreeBusy.java new file mode 100644 index 0000000000..6bcc77b5d2 --- /dev/null +++ b/app/src/main/java/biweekly/property/FreeBusy.java @@ -0,0 +1,131 @@ +package biweekly.property; + +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.parameter.FreeBusyType; +import biweekly.util.Period; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a person's availability over certain time periods (for example, + * "busy" between 1pm-3pm and 4pm-5pm). Note that this property can contain + * multiple time periods, but only one availability type may be defined (e.g. + * "busy" or "free"). + *

+ *

+ * Code sample: + *

+ * + *
+ * VFreeBusy fb = new VFreeBusy();
+ * 
+ * FreeBusy freebusy = new FreeBusy();
+ * freebusy.setType(FreeBusyType.BUSY);
+ * 
+ * Date onePM = ...
+ * Date threePM = ...
+ * freebusy.getValues().add(new Period(onePM, threePM));
+ * 
+ * fb.addFreeBusy(freebusy);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.100-1 + * @see RFC 2445 p.95-6 + */ +public class FreeBusy extends ListProperty { + public FreeBusy() { + //empty + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public FreeBusy(FreeBusy original) { + super(original); + values.clear(); + for (Period period : original.values) { + values.add(new Period(period)); + } + } + + /** + * Gets the person's status over the time periods that are specified in this + * property (for example, "free" or "busy"). If not set, the user should be + * considered "busy". + * @return the type or null if not set + * @see RFC 5545 + * p.20 + */ + public FreeBusyType getType() { + return parameters.getFreeBusyType(); + } + + /** + * Sets the person's status over the time periods that are specified in this + * property (for example, "free" or "busy"). If not set, the user should be + * considered "busy". + * @param fbType the type or null to remove + * @see RFC 5545 + * p.20 + */ + public void setType(FreeBusyType fbType) { + parameters.setFreeBusyType(fbType); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + if (values.isEmpty()) { + return; + } + + for (Period timePeriod : values) { + if (timePeriod.getStartDate() == null) { + warnings.add(new ValidationWarning(39)); + break; + } + } + + for (Period timePeriod : values) { + if (timePeriod.getEndDate() == null && timePeriod.getDuration() == null) { + warnings.add(new ValidationWarning(40)); + break; + } + } + } + + @Override + public FreeBusy copy() { + return new FreeBusy(this); + } +} diff --git a/app/src/main/java/biweekly/property/Geo.java b/app/src/main/java/biweekly/property/Geo.java new file mode 100644 index 0000000000..d370046af6 --- /dev/null +++ b/app/src/main/java/biweekly/property/Geo.java @@ -0,0 +1,168 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a set of geographical coordinates. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Geo geo = new Geo(40.714623, -74.006605);
+ * event.setGeo(geo);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.85-7 + * @see RFC 2445 p.82-3 + * @see vCal 1.0 p.23 + */ +public class Geo extends ICalProperty { + private Double latitude; + private Double longitude; + + /** + * Creates a new geo property. + * @param latitude the latitude + * @param longitude the longitude + */ + public Geo(Double latitude, Double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Geo(Geo original) { + super(original); + latitude = original.latitude; + longitude = original.longitude; + } + + /** + * Gets the latitude. + * @return the latitude + */ + public Double getLatitude() { + return latitude; + } + + /** + * Sets the latitude. + * @param latitude the latitude + */ + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + /** + * Gets the longitude. + * @return the longitude + */ + public Double getLongitude() { + return longitude; + } + + /** + * Sets the longitude. + * @param longitude the longitude + */ + public void setLongitude(Double longitude) { + this.longitude = longitude; + } + + /** + * Converts a coordinate in the degrees-minutes-seconds format into its + * decimal equivalent. + * @param degrees the degrees + * @param minutes the minutes + * @param seconds the seconds + * @return the decimal value + */ + public static double toDecimal(int degrees, int minutes, int seconds) { + return degrees + (minutes / 60.0) + (seconds / 3600.0); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (latitude == null) { + warnings.add(new ValidationWarning(41)); + } + if (longitude == null) { + warnings.add(new ValidationWarning(42)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("latitude", latitude); + values.put("longitude", longitude); + return values; + } + + @Override + public Geo copy() { + return new Geo(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((latitude == null) ? 0 : latitude.hashCode()); + result = prime * result + ((longitude == null) ? 0 : longitude.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Geo other = (Geo) obj; + if (latitude == null) { + if (other.latitude != null) return false; + } else if (!latitude.equals(other.latitude)) return false; + if (longitude == null) { + if (other.longitude != null) return false; + } else if (!longitude.equals(other.longitude)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/ICalProperty.java b/app/src/main/java/biweekly/property/ICalProperty.java new file mode 100644 index 0000000000..919dd98619 --- /dev/null +++ b/app/src/main/java/biweekly/property/ICalProperty.java @@ -0,0 +1,458 @@ +package biweekly.property; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ICalendar; +import biweekly.Messages; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.parameter.ICalParameters; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Base class for all iCalendar property classes. + * @author Michael Angstadt + */ +public abstract class ICalProperty { + /** + * The property parameters. + */ + protected ICalParameters parameters; + + public ICalProperty() { + parameters = new ICalParameters(); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + protected ICalProperty(ICalProperty original) { + parameters = new ICalParameters(original.parameters); + } + + /** + * Gets the property's parameters. + * @return the parameters + */ + public ICalParameters getParameters() { + return parameters; + } + + /** + * Sets the property's parameters + * @param parameters the parameters (cannot be null) + */ + public void setParameters(ICalParameters parameters) { + if (parameters == null) { + throw new NullPointerException(Messages.INSTANCE.getExceptionMessage(16)); + } + this.parameters = parameters; + } + + /** + * Gets the first value of a parameter with the given name. + * @param name the parameter name (case insensitive, e.g. "LANGUAGE") + * @return the parameter value or null if not found + */ + public String getParameter(String name) { + return parameters.first(name); + } + + /** + * Gets all values of a parameter with the given name. + * @param name the parameter name (case insensitive, e.g. "LANGUAGE") + * @return the parameter values (this list is immutable) + */ + public List getParameters(String name) { + return Collections.unmodifiableList(parameters.get(name)); + } + + /** + * Adds a value to a parameter. + * @param name the parameter name (case insensitive, e.g. "LANGUAGE") + * @param value the parameter value + */ + public void addParameter(String name, String value) { + parameters.put(name, value); + } + + /** + * Replaces all existing values of a parameter with the given value. + * @param name the parameter name (case insensitive, e.g. "LANGUAGE") + * @param value the parameter value + */ + public void setParameter(String name, String value) { + parameters.replace(name, value); + } + + /** + * Replaces all existing values of a parameter with the given values. + * @param name the parameter name (case insensitive, e.g. "LANGUAGE") + * @param values the parameter values + */ + public void setParameter(String name, Collection values) { + parameters.replace(name, values); + } + + /** + * Removes a parameter from the property. + * @param name the parameter name (case insensitive, e.g. "LANGUAGE") + */ + public void removeParameter(String name) { + parameters.removeAll(name); + } + + /* + * Note: The following parameter helper methods are package-scoped so that + * property classes can choose which ones they want to make public on their + * Javadoc page. Doing this also allows for the methods' Javadocs to be + * defined in one place. + */ + + /** + * Gets a URI pointing to additional information about the entity + * represented by the property. + * @return the URI or null if not set + * @see RFC 5545 + * p.14-5 + */ + String getAltRepresentation() { + return parameters.getAltRepresentation(); + } + + /** + * Sets a URI pointing to additional information about the entity + * represented by the property. + * @param uri the URI or null to remove + * @see RFC 5545 + * p.14-5 + */ + void setAltRepresentation(String uri) { + parameters.setAltRepresentation(uri); + } + + /** + * Gets the content-type of the property's value. + * @return the content type (e.g. "image/png") or null if not set + * @see RFC 5545 + * p.19-20 + */ + String getFormatType() { + return parameters.getFormatType(); + } + + /** + * Sets the content-type of the property's value. + * @param formatType the content type (e.g. "image/png") or null to remove + * @see RFC 5545 + * p.19-20 + */ + void setFormatType(String formatType) { + parameters.setFormatType(formatType); + } + + /** + * Gets the human-readable label for this property. + * @return the label or null if not set + * @see draft-ietf-calext-extensions-01 + * p.16 + */ + String getLabel() { + return parameters.getLabel(); + } + + /** + * Sets the human-readable label for this property. + * @param label the label or null to remove + * @see draft-ietf-calext-extensions-01 + * p.16 + */ + void setLabel(String label) { + parameters.setLabel(label); + } + + /** + * Gets the language that the property value is written in. + * @return the language (e.g. "en" for English) or null if not set + * @see RFC 5545 + * p.21 + */ + String getLanguage() { + return parameters.getLanguage(); + } + + /** + * Sets the language that the property value is written in. + * @param language the language (e.g. "en" for English) or null to remove + * @see RFC 5545 + * p.21 + */ + void setLanguage(String language) { + parameters.setLanguage(language); + } + + /** + *

+ * Gets a URI which represents a person who is acting on behalf of the + * person that is defined in this property. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return a URI representing the person (typically, an email URI, e.g. + * "mailto:janedoe@example.com") or null if not set + * @see RFC 5545 + * p.27 + */ + String getSentBy() { + return parameters.getSentBy(); + } + + /** + *

+ * Sets a URI which represents a person who is acting on behalf of the + * person that is defined in this property. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @param uri a URI representing the person (typically, an email URI, e.g. + * "mailto:janedoe@example.com") or null to remove + * @see RFC 5545 + * p.27 + */ + void setSentBy(String uri) { + parameters.setSentBy(uri); + } + + /** + *

+ * Gets the human-readable, display name of the entity represented by this + * property. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return the display name (e.g. "John Doe") or null if not set + * @see RFC 5545 + * p.15-6 + */ + String getCommonName() { + return parameters.getCommonName(); + } + + /** + *

+ * Sets the human-readable, display name of the entity represented by this + * property. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @param commonName the display name (e.g. "John Doe") or null to remove + * @see RFC 5545 + * p.15-6 + */ + void setCommonName(String commonName) { + parameters.setCommonName(commonName); + } + + /** + *

+ * Gets a URI that contains additional information about the person. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @return the URI (e.g. an LDAP URI) or null if not set + * @see RFC 5545 + * p.18 + */ + String getDirectoryEntry() { + return parameters.getDirectoryEntry(); + } + + /** + *

+ * Sets a URI that contains additional information about the person. + *

+ *

+ * Supported versions: {@code 2.0} + *

+ * @param uri the URI (e.g. an LDAP URI) or null to remove + * @see RFC 5545 + * p.18 + */ + void setDirectoryEntry(String uri) { + parameters.setDirectoryEntry(uri); + } + + /** + *

+ * Checks the property for data consistency problems or deviations from the + * specifications. + *

+ *

+ * The existence of validation warnings will not prevent the property object + * from being written to a data stream. Syntactically-correct output will + * still be produced. However, the consuming application may have trouble + * interpreting some of the data due to the presence of these warnings. + *

+ *

+ * These problems can largely be avoided by reading the Javadocs of the + * component and property classes, or by being familiar with the iCalendar + * standard. + *

+ * @param components the hierarchy of components that the property belongs + * to + * @param version the version to validate against + * @see ICalendar#validate(List, ICalVersion) + * @return a list of warnings or an empty list if no problems were found + */ + public final List validate(List components, ICalVersion version) { + //validate property value + List warnings = new ArrayList(0); + validate(components, version, warnings); + + //validate parameters + warnings.addAll(parameters.validate(version)); + + return warnings; + } + + /** + *

+ * Checks the property for data consistency problems or deviations from the + * specifications. + *

+ *

+ * This method should be overridden by child classes that wish to provide + * validation logic. The default implementation of this method does nothing. + *

+ * @param components the hierarchy of components that the property belongs + * to + * @param version the version to validate against + * @param warnings the list to add the warnings to + */ + protected void validate(List components, ICalVersion version, List warnings) { + //do nothing + } + + /** + *

+ * Gets string representations of the class's fields for the + * {@link #toString} method. + *

+ *

+ * Meant to be overridden by child classes. The default implementation + * returns an empty map. + *

+ * @return the values of the class's fields (key = field name, value = field + * value) + */ + protected Map toStringValues() { + return Collections.emptyMap(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()); + sb.append(" [ parameters=").append(parameters); + for (Map.Entry field : toStringValues().entrySet()) { + String fieldName = field.getKey(); + Object fieldValue = field.getValue(); + sb.append(" | ").append(fieldName).append('=').append(fieldValue); + } + sb.append(" ]"); + return sb.toString(); + } + + /** + *

+ * Creates a copy of this property object. + *

+ *

+ * The default implementation of this method uses reflection to look for a + * copy constructor. Child classes SHOULD override this method to avoid the + * performance overhead involved in using reflection. + *

+ *

+ * The child class's copy constructor, if present, MUST invoke the + * {@link #ICalProperty(ICalProperty)} super constructor to ensure that the + * parameters are also copied. + *

+ *

+ * This method MUST be overridden by the child class if the child class does + * not have a copy constructor. Otherwise, an + * {@link UnsupportedOperationException} will be thrown when an attempt is + * made to copy the property (such as in the + * {@link ICalendar#ICalendar(ICalendar) ICalendar class's copy constructor} + * ). + *

+ * @return the copy + * @throws UnsupportedOperationException if the class does not have a copy + * constructor or there is a problem invoking it + */ + public ICalProperty copy() { + Class clazz = getClass(); + + try { + Constructor copyConstructor = clazz.getConstructor(clazz); + return copyConstructor.newInstance(this); + } catch (Exception e) { + throw new UnsupportedOperationException(Messages.INSTANCE.getExceptionMessage(17, clazz.getName()), e); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + parameters.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ICalProperty other = (ICalProperty) obj; + if (!parameters.equals(other.parameters)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Image.java b/app/src/main/java/biweekly/property/Image.java new file mode 100644 index 0000000000..de9b57cda4 --- /dev/null +++ b/app/src/main/java/biweekly/property/Image.java @@ -0,0 +1,146 @@ +package biweekly.property; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.parameter.Display; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines an image that is associated with the component that the property + * belongs to. Multiple instances with different DISPLAY parameters can be added + * to the component to define different images for the client to display in + * different circumstances. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //from a byte array
+ * byte[] data = ...
+ * Image image = new Image("image/png", data);
+ * image.getDisplays().add(Display.BADGE);
+ * event.addImage(image);
+ * 
+ * //referencing a URL
+ * image = new Image("image/png", "http://example.com/image.png");
+ * image.getDisplays().add(Display.THUMBNAIL);
+ * image.setOnClickUri("http://example.com");
+ * event.addImage(image);
+ * 
+ * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.10 + */ +public class Image extends BinaryProperty { + /** + * Creates a new attachment. + * @param formatType the content-type of the data (e.g. "image/png") + * @param file the file to attach + * @throws IOException if there's a problem reading from the file + */ + public Image(String formatType, File file) throws IOException { + super(file); + setFormatType(formatType); + } + + /** + * Creates a new attachment. + * @param formatType the content-type of the data (e.g. "image/png") + * @param data the binary data + */ + public Image(String formatType, byte[] data) { + super(data); + setFormatType(formatType); + } + + /** + * Creates a new attachment. + * @param formatType the content-type of the data (e.g. "image/png") + * @param uri a URL pointing to the resource (e.g. + * "http://example.com/image.png") + */ + public Image(String formatType, String uri) { + super(uri); + setFormatType(formatType); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Image(Image original) { + super(original); + } + + /** + * Gets the URI to go to when the user clicks on the image. + * @return the URI or null if not set + */ + public String getOnClickUri() { + return parameters.getAltRepresentation(); + } + + /** + * Sets the URI to go to when the user clicks on the image. + * @param uri the URI or null to remove + */ + public void setOnClickUri(String uri) { + parameters.setAltRepresentation(uri); + } + + /** + * Gets the list that holds the ways in which the client should display this + * image (for example, as a thumbnail-sized image). + * @return the display methods (this list is mutable) + */ + public List getDisplays() { + return parameters.getDisplays(); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + if (data != null && getFormatType() == null) { + warnings.add(new ValidationWarning(56)); + } + } + + @Override + public Image copy() { + return new Image(this); + } +} diff --git a/app/src/main/java/biweekly/property/IntegerProperty.java b/app/src/main/java/biweekly/property/IntegerProperty.java new file mode 100644 index 0000000000..04ae26c650 --- /dev/null +++ b/app/src/main/java/biweekly/property/IntegerProperty.java @@ -0,0 +1,48 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is an integer. + * @author Michael Angstadt + */ +public class IntegerProperty extends ValuedProperty { + /** + * Creates a new integer property. + * @param value the property's value + */ + public IntegerProperty(Integer value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public IntegerProperty(IntegerProperty original) { + super(original); + } +} diff --git a/app/src/main/java/biweekly/property/LastModified.java b/app/src/main/java/biweekly/property/LastModified.java new file mode 100644 index 0000000000..9dcf65b0cf --- /dev/null +++ b/app/src/main/java/biweekly/property/LastModified.java @@ -0,0 +1,74 @@ +package biweekly.property; + +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the time that the calendar data in a component was last changed. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Date datetime = ...
+ * LastModified lastModified = new LastModified(datetime);
+ * event.setLastModified(lastModified);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.138 + * @see RFC 2445 p.131 + * @see vCal 1.0 p.31 + * @see draft-ietf-calext-extensions-01 + * p.7 + */ +public class LastModified extends DateTimeProperty { + /** + * Creates a last modified property. + * @param date the date + */ + public LastModified(Date date) { + super(date); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public LastModified(LastModified original) { + super(original); + } + + @Override + public LastModified copy() { + return new LastModified(this); + } +} diff --git a/app/src/main/java/biweekly/property/ListProperty.java b/app/src/main/java/biweekly/property/ListProperty.java new file mode 100644 index 0000000000..530e033db1 --- /dev/null +++ b/app/src/main/java/biweekly/property/ListProperty.java @@ -0,0 +1,120 @@ +package biweekly.property; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.Messages; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is a list of values. + * @author Michael Angstadt + * @param the value type + */ +public class ListProperty extends ICalProperty { + protected final List values; + + /** + * Creates a new list property. + */ + public ListProperty() { + values = new ArrayList(); + } + + /** + * Creates a new list property. + * @param values the values to initialize the property with + */ + public ListProperty(T... values) { + this.values = new ArrayList(Arrays.asList(values)); + } + + /** + * Creates a new list property. + * @param values the values to initialize the property with (cannot be null) + */ + public ListProperty(List values) { + if (values == null) { + throw new NullPointerException(Messages.INSTANCE.getExceptionMessage(18)); + } + this.values = values; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public ListProperty(ListProperty original) { + super(original); + values = new ArrayList(original.values); + } + + /** + * Gets the list that holds the values of this property. + * @return the values list (this list is mutable) + */ + public List getValues() { + return values; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (values.isEmpty()) { + warnings.add(new ValidationWarning(26)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("values", this.values); + return values; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + values.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + ListProperty other = (ListProperty) obj; + if (!values.equals(other.values)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Location.java b/app/src/main/java/biweekly/property/Location.java new file mode 100644 index 0000000000..e60c9c95f4 --- /dev/null +++ b/app/src/main/java/biweekly/property/Location.java @@ -0,0 +1,88 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the physical location of an event. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Location location = new Location("Room 32B");
+ * event.setLocation(location);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.87-8 + * @see RFC 2445 p.84 + * @see vCal 1.0 p.32 + */ +public class Location extends TextProperty { + /** + * Creates a location property. + * @param location the location (e.g. "Room 101") + */ + public Location(String location) { + super(location); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Location(Location original) { + super(original); + } + + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Location copy() { + return new Location(this); + } +} diff --git a/app/src/main/java/biweekly/property/Method.java b/app/src/main/java/biweekly/property/Method.java new file mode 100644 index 0000000000..c09e3fcd57 --- /dev/null +++ b/app/src/main/java/biweekly/property/Method.java @@ -0,0 +1,227 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Specifies the type of iTIP + * request that the iCalendar object represents. If the iCalendar object is just + * being used as a container to hold calendar information, then this property + * does not need to be defined. + *

+ *

+ * If the iCalendar object is defined as a MIME message entity, this property + * MUST be set to the value of the "Content-Type" header's "method" parameter, + * if present. + *

+ *

+ * Code sample: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * Method method = Method.request();
+ * ical.setMethod(method);
+ * 
+ * @author Michael Angstadt + * @see RFC 5546 + * @see RFC 5545 p.77-8 + * @see RFC 2445 p.74-5 + */ +public class Method extends EnumProperty { + public static final String ADD = "ADD"; + public static final String CANCEL = "CANCEL"; + public static final String COUNTER = "COUNTER"; + public static final String DECLINECOUNTER = "DECLINECOUNTER"; + public static final String PUBLISH = "PUBLISH"; + public static final String REFRESH = "REFRESH"; + public static final String REPLY = "REPLY"; + public static final String REQUEST = "REQUEST"; + + /** + * Creates a new method property. + * @param value the property value + */ + public Method(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Method(Method original) { + super(original); + } + + /** + * Constructs a METHOD property whose value is "ADD". + * @return the property + */ + public static Method add() { + return create(ADD); + } + + /** + * Determines if this property's value is "ADD". + * @return true if the value is "ADD", false if not + */ + public boolean isAdd() { + return is(ADD); + } + + /** + * Constructs a METHOD property whose value is "CANCEL". + * @return the property + */ + public static Method cancel() { + return create(CANCEL); + } + + /** + * Determines if this property's value is "CANCEL". + * @return true if the value is "CANCEL", false if not + */ + public boolean isCancel() { + return is(CANCEL); + } + + /** + * Constructs a METHOD property whose value is "COUNTER". + * @return the property + */ + public static Method counter() { + return create(COUNTER); + } + + /** + * Determines if this property's value is "COUNTER". + * @return true if the value is "COUNTER", false if not + */ + public boolean isCounter() { + return is(COUNTER); + } + + /** + * Constructs a METHOD property whose value is "DECLINECOUNTER". + * @return the property + */ + public static Method declineCounter() { + return create(DECLINECOUNTER); + } + + /** + * Determines if this property's value is "DECLINECOUNTER". + * @return true if the value is "DECLINECOUNTER", false if not + */ + public boolean isDeclineCounter() { + return is(DECLINECOUNTER); + } + + /** + * Constructs a METHOD property whose value is "PUBLISH". + * @return the property + */ + public static Method publish() { + return create(PUBLISH); + } + + /** + * Determines if this property's value is "PUBLISH". + * @return true if the value is "PUBLISH", false if not + */ + public boolean isPublish() { + return is(PUBLISH); + } + + /** + * Constructs a METHOD property whose value is "REFRESH". + * @return the property + */ + public static Method refresh() { + return create(REFRESH); + } + + /** + * Determines if this property's value is "REFRESH". + * @return true if the value is "REFRESH", false if not + */ + public boolean isRefresh() { + return is(REFRESH); + } + + /** + * Constructs a METHOD property whose value is "REPLY". + * @return the property + */ + public static Method reply() { + return create(REPLY); + } + + /** + * Determines if this property's value is "REPLY". + * @return true if the value is "REPLY", false if not + */ + public boolean isReply() { + return is(REPLY); + } + + /** + * Constructs a METHOD property whose value is "REQUEST". + * @return the property + */ + public static Method request() { + return create(REQUEST); + } + + /** + * Determines if this property's value is "REQUEST". + * @return true if the value is "REQUEST", false if not + */ + public boolean isRequest() { + return is(REQUEST); + } + + private static Method create(String value) { + return new Method(value); + } + + @Override + protected Collection getStandardValues(ICalVersion version) { + return Arrays.asList(ADD, CANCEL, COUNTER, DECLINECOUNTER, PUBLISH, REFRESH, REPLY, REQUEST); + } + + @Override + public Method copy() { + return new Method(this); + } +} diff --git a/app/src/main/java/biweekly/property/Name.java b/app/src/main/java/biweekly/property/Name.java new file mode 100644 index 0000000000..4591590bbd --- /dev/null +++ b/app/src/main/java/biweekly/property/Name.java @@ -0,0 +1,111 @@ +package biweekly.property; + +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a human-readable name for the calendar as a whole. + *

+ *

+ * An {@link ICalendar} component can only have one name, but multiple Name + * properties can exist in order to specify the name in multiple languages. In + * this case, each property instance must be assigned a LANGUAGE parameter. + *

+ *

+ * Single language: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * Name name = new Name("Company Vacation Days");
+ * ical.addName(name);
+ * 
+ * + *

+ * Multiple languages: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * Name englishName = new Name("Company Vacation Days");
+ * englishName.setLanguage("en");
+ * ical.addName(englishName);
+ * 
+ * Name frenchName = new Name("Société Jours de Vacances");
+ * frenchName.setLanguage("fr");
+ * ical.addName(frenchName);
+ * 
+ * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.5 + */ +public class Name extends TextProperty { + /** + * Creates a name property. + * @param name the name of the calendar + */ + public Name(String name) { + super(name); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Name(Name original) { + super(original); + } + + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Name copy() { + return new Name(this); + } +} diff --git a/app/src/main/java/biweekly/property/Organizer.java b/app/src/main/java/biweekly/property/Organizer.java new file mode 100644 index 0000000000..a9a1f7804e --- /dev/null +++ b/app/src/main/java/biweekly/property/Organizer.java @@ -0,0 +1,206 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.Map; + +import biweekly.component.VEvent; +import biweekly.component.VFreeBusy; +import biweekly.component.VJournal; +import biweekly.component.VTodo; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * This property has different meanings depending on the component it belongs + * to: + *

+ *
    + *
  • {@link VEvent} - The organizer of the event.
  • + *
  • {@link VTodo} - The creator of the to-do task.
  • + *
  • {@link VJournal} - The owner of the journal entry.
  • + *
  • {@link VFreeBusy} - The person requesting the free/busy time.
  • + *
+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Organizer organizer = Organizer.email("johndoe@example.com");
+ * organizer.setCommonName("John Doe");
+ * event.setOrganizer(organizer);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.111-2 + * @see RFC 2445 + * p.106-7 + */ +public class Organizer extends ICalProperty { + private String uri, email, name; + + /** + * Creates an organizer property + * @param name the organizer's name (e.g. "John Doe") + * @param email the organizer's email address (e.g. "jdoe@example.com") + */ + public Organizer(String name, String email) { + this.name = name; + this.email = email; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Organizer(Organizer original) { + super(original); + name = original.name; + email = original.email; + uri = original.uri; + } + + /** + * Gets the organizer's email + * @return the email (e.g. "jdoe@company.com") + */ + public String getEmail() { + return email; + } + + /** + * Sets the organizer's email + * @param email the email (e.g. "jdoe@company.com") + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Gets a URI representing the organizer. + * @return the URI (e.g. "mailto:jdoe@company.com") + */ + public String getUri() { + return uri; + } + + /** + * Sets a URI representing the organizer. + * @param uri the URI (e.g. "mailto:jdoe@company.com") + */ + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public String getSentBy() { + return super.getSentBy(); + } + + @Override + public void setSentBy(String sentBy) { + super.setSentBy(sentBy); + } + + @Override + public String getCommonName() { + return name; + } + + @Override + public void setCommonName(String commonName) { + this.name = commonName; + } + + @Override + public String getDirectoryEntry() { + return super.getDirectoryEntry(); + } + + @Override + public void setDirectoryEntry(String directoryEntry) { + super.setDirectoryEntry(directoryEntry); + } + + /** + * Gets the language that the common name parameter is written in. + */ + @Override + public String getLanguage() { + return super.getLanguage(); + } + + /** + * Sets the language that the common name parameter is written in. + */ + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("name", name); + values.put("email", email); + values.put("uri", uri); + return values; + } + + @Override + public Organizer copy() { + return new Organizer(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((email == null) ? 0 : email.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Organizer other = (Organizer) obj; + if (email == null) { + if (other.email != null) return false; + } else if (!email.equals(other.email)) return false; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + if (uri == null) { + if (other.uri != null) return false; + } else if (!uri.equals(other.uri)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/PercentComplete.java b/app/src/main/java/biweekly/property/PercentComplete.java new file mode 100644 index 0000000000..a8a0e8c87e --- /dev/null +++ b/app/src/main/java/biweekly/property/PercentComplete.java @@ -0,0 +1,81 @@ +package biweekly.property; + +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a to-do task's level of completion. + *

+ *

+ * Code sample: + *

+ * + *
+ * VTodo todo = new VTodo();
+ * 
+ * PercentComplete percentComplete = new PercentComplete(50); //50%
+ * todo.setPercentComplete(percentComplete);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.88-9 + * @see RFC 2445 p.85 + */ +public class PercentComplete extends IntegerProperty { + /** + * Creates a percent complete property. + * @param percent the percentage (e.g. "50" for 50%) + */ + public PercentComplete(Integer percent) { + super(percent); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public PercentComplete(PercentComplete original) { + super(original); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + if (value != null && (value < 0 || value > 100)) { + warnings.add(new ValidationWarning(29, value)); + } + } + + @Override + public PercentComplete copy() { + return new PercentComplete(this); + } +} diff --git a/app/src/main/java/biweekly/property/Priority.java b/app/src/main/java/biweekly/property/Priority.java new file mode 100644 index 0000000000..b4f149c190 --- /dev/null +++ b/app/src/main/java/biweekly/property/Priority.java @@ -0,0 +1,116 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the priority of an event or to-do task. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * event.setPriority(1); //highest
+ * event.setPriority(9); //lowest
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.89-90 + * @see RFC 2445 p.85-7 + * @see vCal 1.0 p.33 + */ +public class Priority extends IntegerProperty { + /** + * Creates a priority property. + * @param priority the priority ("0" is undefined, "1" is the highest, "9" + * is the lowest) + */ + public Priority(Integer priority) { + super(priority); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Priority(Priority original) { + super(original); + } + + /** + * Determines if this priority is considered "high" priority. + * @return true if the priority is between 1 and 4, false if not + */ + public boolean isHigh() { + return value != null && value >= 1 && value <= 4; + } + + /** + * Determines if this priority is considered "medium" priority. + * @return true if the priority is "5", false if not + */ + public boolean isMedium() { + return value != null && value == 5; + } + + /** + * Determines if this priority is considered "low" priority. + * @return true if the priority is between 6 and 9, false if not + */ + public boolean isLow() { + return value != null && value >= 6 && value <= 9; + } + + /** + * Determines if this priority has an "undefined" value. + * @return true if the priority is "0", false if not + */ + public boolean isUndefined() { + return value != null && value == 0; + } + + /** + * Converts this priority to its two-character CUA code. + * @return the CUA code (e.g. "B1" for "4") or null if the priority cannot + * be converted to a CUA code + */ + public String toCuaPriority() { + if (value == null || value < 1 || value > 9) { + return null; + } + int letter = ((value - 1) / 3) + 'A'; + int number = ((value - 1) % 3) + 1; + return (char) letter + "" + number; + } + + @Override + public Priority copy() { + return new Priority(this); + } +} diff --git a/app/src/main/java/biweekly/property/ProcedureAlarm.java b/app/src/main/java/biweekly/property/ProcedureAlarm.java new file mode 100644 index 0000000000..65bdf56d21 --- /dev/null +++ b/app/src/main/java/biweekly/property/ProcedureAlarm.java @@ -0,0 +1,107 @@ +package biweekly.property; + +import java.util.Map; + +import biweekly.component.VAlarm; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines an alarm that executes a procedure when triggered. It is recommended + * that the {@link VAlarm} component be used to created alarms. + * @author Michael Angstadt + * @see vCal 1.0 p.33 + * @see VAlarm#procedure + */ +public class ProcedureAlarm extends VCalAlarmProperty { + private String path; + + /** + * @param path the path or name of the procedure to run when the alarm is + * triggered + */ + public ProcedureAlarm(String path) { + this.path = path; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public ProcedureAlarm(ProcedureAlarm original) { + super(original); + path = original.path; + } + + /** + * Gets the path or name of the procedure to run when the alarm is + * triggered. + * @return the path + */ + public String getPath() { + return path; + } + + /** + * Sets the path or name of the procedure to run when the alarm is + * triggered. + * @param path the path + */ + public void getPath(String path) { + this.path = path; + } + + @Override + protected Map toStringValues() { + Map values = super.toStringValues(); + values.put("path", path); + return values; + } + + @Override + public ProcedureAlarm copy() { + return new ProcedureAlarm(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + ProcedureAlarm other = (ProcedureAlarm) obj; + if (path == null) { + if (other.path != null) return false; + } else if (!path.equals(other.path)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/ProductId.java b/app/src/main/java/biweekly/property/ProductId.java new file mode 100644 index 0000000000..4917262da7 --- /dev/null +++ b/app/src/main/java/biweekly/property/ProductId.java @@ -0,0 +1,79 @@ +package biweekly.property; + +import biweekly.Biweekly; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Identifies the application that created the iCalendar object. + *

+ *

+ * Code sample: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * ProductId prodid = new ProductId("-//Company//Application Name//EN");
+ * ical.setProductId(prodid);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.78-9 + * @see RFC 2445 p.75-6 + * @see vCal 1.0 p.24 + */ +public class ProductId extends TextProperty { + /** + * Creates a new product identifier property. + * @param value a unique string representing the application (e.g. + * "-//Company//Application Name//EN") + */ + public ProductId(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public ProductId(ProductId original) { + super(original); + } + + /** + * Creates a new product identifier property that represents this library. + * @return the property + */ + public static ProductId biweekly() { + return new ProductId("-//Michael Angstadt//biweekly " + Biweekly.VERSION + "//EN"); + } + + @Override + public ProductId copy() { + return new ProductId(this); + } +} diff --git a/app/src/main/java/biweekly/property/RawProperty.java b/app/src/main/java/biweekly/property/RawProperty.java new file mode 100644 index 0000000000..1f7053e8ca --- /dev/null +++ b/app/src/main/java/biweekly/property/RawProperty.java @@ -0,0 +1,183 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalDataType; +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +import com.github.mangstadt.vinnie.SyntaxStyle; +import com.github.mangstadt.vinnie.validate.AllowedCharacters; +import com.github.mangstadt.vinnie.validate.VObjectValidator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property that does not have a scribe associated with it. + * @author Michael Angstadt + */ +public class RawProperty extends ICalProperty { + private String name; + private ICalDataType dataType; + private String value; + + /** + * Creates a raw property. + * @param name the property name (e.g. "X-MS-ANNIVERSARY") + * @param value the property value + */ + public RawProperty(String name, String value) { + this(name, null, value); + } + + /** + * Creates a raw property. + * @param name the property name (e.g. "X-MS-ANNIVERSARY") + * @param dataType the property value's data type + * @param value the property value + */ + public RawProperty(String name, ICalDataType dataType, String value) { + this.name = name; + this.dataType = dataType; + this.value = value; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RawProperty(RawProperty original) { + super(original); + name = original.name; + dataType = original.dataType; + value = original.value; + } + + /** + * Gets the property value. + * @return the property value + */ + public String getValue() { + return value; + } + + /** + * Sets the property value. + * @param value the property value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the property value's data type. + * @return the data type + */ + public ICalDataType getDataType() { + return dataType; + } + + /** + * Sets the property value's data type. + * @param dataType the data type + */ + public void setDataType(ICalDataType dataType) { + this.dataType = dataType; + } + + /** + * Gets the property name. + * @return the property name (e.g. "X-MS-ANNIVERSARY") + */ + public String getName() { + return name; + } + + /** + * Sets the property name. + * @param name the property name (e.g. "X-MS-ANNIVERSARY") + */ + public void setName(String name) { + this.name = name; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + SyntaxStyle syntax = version.getSyntaxStyle(); + AllowedCharacters allowed = VObjectValidator.allowedCharactersParameterName(syntax, true); + if (!allowed.check(name)) { + if (syntax == SyntaxStyle.OLD) { + AllowedCharacters notAllowed = allowed.flip(); + warnings.add(new ValidationWarning(59, name, notAllowed.toString(true))); + } else { + warnings.add(new ValidationWarning(52, name)); + } + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("name", name); + values.put("value", value); + values.put("dataType", dataType); + return values; + } + + @Override + public RawProperty copy() { + return new RawProperty(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((dataType == null) ? 0 : dataType.hashCode()); + result = prime * result + ((name == null) ? 0 : name.toLowerCase().hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + RawProperty other = (RawProperty) obj; + if (dataType != other.dataType) return false; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equalsIgnoreCase(other.name)) return false; + if (value == null) { + if (other.value != null) return false; + } else if (!value.equals(other.value)) return false; + return true; + } + +} diff --git a/app/src/main/java/biweekly/property/RecurrenceDates.java b/app/src/main/java/biweekly/property/RecurrenceDates.java new file mode 100644 index 0000000000..649428296a --- /dev/null +++ b/app/src/main/java/biweekly/property/RecurrenceDates.java @@ -0,0 +1,180 @@ +package biweekly.property; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.ICalDate; +import biweekly.util.Period; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a list of dates or time periods that help define a recurrence rule. + * It must contain either dates or time periods. It cannot contain a combination + * of both. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //date-time values
+ * Date datetime = ...
+ * RecurrenceDates rdate = new RecurrenceDates();
+ * rdate.getDates().add(new ICalDate(datetime, true));
+ * event.addRecurrenceDates(rdate);
+ * 
+ * //date values
+ * Date date = ...
+ * RecurrenceDates rdate = new RecurrenceDates();
+ * rdate.getDates().add(new ICalDate(date, false));
+ * event.addRecurrenceDates(rdate);
+ * 
+ * //periods
+ * Period period = ...
+ * rdate = new RecurrenceDates();
+ * rdate.getPeriods().add(period);
+ * event.addRecurrenceDates(rdate);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.120-2 + * @see RFC 2445 + * p.115-7 + * @see vCal 1.0 p.34 + */ +public class RecurrenceDates extends ICalProperty { + private final List dates; + private final List periods; + + public RecurrenceDates() { + dates = new ArrayList(); + periods = new ArrayList(); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RecurrenceDates(RecurrenceDates original) { + super(original); + + dates = new ArrayList(original.dates.size()); + for (ICalDate date : original.dates) { + dates.add(new ICalDate(date)); + } + + periods = new ArrayList(original.periods.size()); + for (Period period : original.periods) { + periods.add(new Period(period)); + } + } + + /** + * Gets the list that stores this property's recurrence dates. + * @return the dates (this list is mutable) + */ + public List getDates() { + return dates; + } + + /** + * Gets the list that stores this property's time periods. + * @return the time periods (this list is mutable) + */ + public List getPeriods() { + return periods; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (dates.isEmpty() && periods.isEmpty()) { + //no value + warnings.add(new ValidationWarning(26)); + } + + if (!dates.isEmpty() && !periods.isEmpty()) { + //can't mix dates and periods + warnings.add(new ValidationWarning(49)); + } + + if (version == ICalVersion.V1_0 && !periods.isEmpty()) { + //1.0 doesn't support periods + warnings.add(new ValidationWarning(51)); + } + + if (!dates.isEmpty()) { + //can't mix date and date-time values + boolean hasTime = dates.get(0).hasTime(); + for (ICalDate date : dates.subList(1, dates.size())) { + if (date.hasTime() != hasTime) { + warnings.add(new ValidationWarning(50)); + break; + } + } + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("dates", dates); + values.put("periods", periods); + return values; + } + + @Override + public RecurrenceDates copy() { + return new RecurrenceDates(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + dates.hashCode(); + result = prime * result + periods.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + RecurrenceDates other = (RecurrenceDates) obj; + if (!dates.equals(other.dates)) return false; + if (!periods.equals(other.periods)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/RecurrenceId.java b/app/src/main/java/biweekly/property/RecurrenceId.java new file mode 100644 index 0000000000..af1b260f11 --- /dev/null +++ b/app/src/main/java/biweekly/property/RecurrenceId.java @@ -0,0 +1,123 @@ +package biweekly.property; + +import java.util.Date; + +import biweekly.parameter.Range; +import biweekly.util.ICalDate; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Records the original value of the {@link DateStart} property if a recurrence + * instance has been modified. It is used in conjunction with the {@link Uid} + * and {@link Sequence} properties to uniquely identify a recurrence instance. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * //date-time value
+ * Date datetime = ...
+ * RecurrenceId recurrenceId = new RecurrenceId(datetime);
+ * event.setRecurrenceId(recurrenceId);
+ * 
+ * //date value
+ * Date date = ...
+ * RecurrenceId recurrenceId = new RecurrenceId(date, false);
+ * event.setRecurrenceId(recurrenceId);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.112-4 + * @see RFC 2445 + * p.107-9 + */ +public class RecurrenceId extends DateOrDateTimeProperty { + /** + * Creates a recurrence ID property. + * @param originalStartDate the original start date + */ + public RecurrenceId(ICalDate originalStartDate) { + super(originalStartDate); + } + + /** + * Creates a recurrence ID property. + * @param originalStartDate the original start date + */ + public RecurrenceId(Date originalStartDate) { + super(originalStartDate); + } + + /** + * Creates a recurrence ID property. + * @param originalStartDate the original start date + * @param hasTime true to include the time component of the date, false not + * to + */ + public RecurrenceId(Date originalStartDate, boolean hasTime) { + super(originalStartDate, hasTime); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RecurrenceId(RecurrenceId original) { + super(original); + } + + /** + * Gets the effective range of recurrence instances from the instance + * specified by this property. + * @return the range or null if not set + * @see RFC 5545 + * p.23-4 + */ + public Range getRange() { + return parameters.getRange(); + } + + /** + * Sets the effective range of recurrence instances from the instance + * specified by this property. + * @param range the range or null to remove + * @see RFC 5545 + * p.23-4 + */ + public void setRange(Range range) { + parameters.setRange(range); + } + + @Override + public RecurrenceId copy() { + return new RecurrenceId(this); + } +} diff --git a/app/src/main/java/biweekly/property/RecurrenceProperty.java b/app/src/main/java/biweekly/property/RecurrenceProperty.java new file mode 100644 index 0000000000..f86040ae12 --- /dev/null +++ b/app/src/main/java/biweekly/property/RecurrenceProperty.java @@ -0,0 +1,133 @@ +package biweekly.property; + +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.Frequency; +import biweekly.util.Google2445Utils; +import biweekly.util.ICalDate; +import biweekly.util.Recurrence; +import biweekly.util.com.google.ical.compat.javautil.DateIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is a recurrence rule. + * @author Michael Angstadt + */ +public class RecurrenceProperty extends ValuedProperty { + /** + * Creates a new recurrence property. + * @param recur the recurrence value + */ + public RecurrenceProperty(Recurrence recur) { + super(recur); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RecurrenceProperty(RecurrenceProperty original) { + super(original); + } + + /** + * Creates an iterator that computes the dates defined by this property. + * @param startDate the date that the recurrence starts (typically, the + * value of the accompanying {@link DateStart} property) + * @param timezone the timezone to iterate in (typically, the timezone + * associated with the {@link DateStart} property). This is needed in order + * to adjust for when the iterator passes over a daylight savings boundary. + * @return the iterator + * @see google-rfc-2445 + */ + public DateIterator getDateIterator(Date startDate, TimeZone timezone) { + return getDateIterator(new ICalDate(startDate), timezone); + } + + /** + * Creates an iterator that computes the dates defined by this property. + * @param startDate the date that the recurrence starts (typically, the + * value of the accompanying {@link DateStart} property) + * @param timezone the timezone to iterate in (typically, the timezone + * associated with the {@link DateStart} property). This is needed in order + * to adjust for when the iterator passes over a daylight savings boundary. + * @return the iterator + * @see google-rfc-2445 + */ + public DateIterator getDateIterator(ICalDate startDate, TimeZone timezone) { + Recurrence recur = getValue(); + return (recur == null) ? new Google2445Utils.EmptyDateIterator() : recur.getDateIterator(startDate, timezone); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + if (value == null) { + return; + } + + if (value.getFrequency() == null) { + warnings.add(new ValidationWarning(30)); + } + + if (value.getUntil() != null && value.getCount() != null) { + warnings.add(new ValidationWarning(31)); + } + + switch (version) { + case V1_0: + if (!value.getXRules().isEmpty()) { + warnings.add(new ValidationWarning("X-Rules are not supported by vCal.")); + } + if (!value.getBySetPos().isEmpty()) { + warnings.add(new ValidationWarning("BYSETPOS is not supported by vCal.")); + } + if (value.getFrequency() == Frequency.SECONDLY) { + warnings.add(new ValidationWarning(Frequency.SECONDLY.name() + " frequency is not supported by vCal.")); + } + break; + + case V2_0_DEPRECATED: + //empty + break; + + case V2_0: + if (!value.getXRules().isEmpty()) { + warnings.add(new ValidationWarning(32)); + } + + break; + } + } +} diff --git a/app/src/main/java/biweekly/property/RecurrenceRule.java b/app/src/main/java/biweekly/property/RecurrenceRule.java new file mode 100644 index 0000000000..901194f7fd --- /dev/null +++ b/app/src/main/java/biweekly/property/RecurrenceRule.java @@ -0,0 +1,73 @@ +package biweekly.property; + +import biweekly.util.Recurrence; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines how often a component repeats. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Recurrence recur = new Recurrence.Builder(Frequency.WEEKLY).interval(2).build();
+ * RecurrenceRule rrule = new RecurrenceRule(recur);
+ * event.setRecurrenceRule(rruel);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.122-32 + * @see RFC 2445 + * p.117-25 + * @see vCal 1.0 p.34 + */ +public class RecurrenceRule extends RecurrenceProperty { + /** + * Creates a new recurrence rule property. + * @param recur the recurrence rule + */ + public RecurrenceRule(Recurrence recur) { + super(recur); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RecurrenceRule(RecurrenceRule original) { + super(original); + } + + @Override + public RecurrenceRule copy() { + return new RecurrenceRule(this); + } +} diff --git a/app/src/main/java/biweekly/property/RefreshInterval.java b/app/src/main/java/biweekly/property/RefreshInterval.java new file mode 100644 index 0000000000..b54d063f9e --- /dev/null +++ b/app/src/main/java/biweekly/property/RefreshInterval.java @@ -0,0 +1,72 @@ +package biweekly.property; + +import biweekly.util.Duration; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the suggested minimum polling interval for checking for updates to + * the calendar data. + *

+ *

+ * Code sample: + *

+ * + *
+ * ICalendar ical = new ICalendar()
+ * 
+ * Duration duration = Duration.builder().weeks(1).build();
+ * RefreshInterval refreshInterval = new RefreshInterval(duration);
+ * ical.setRefreshInterval(refreshInterval);
+ * 
+ * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.7 + */ +public class RefreshInterval extends ValuedProperty { + /** + * Creates a refresh interval property. + * @param duration the duration value (e.g. "2 hours and 30 minutes") + */ + public RefreshInterval(Duration duration) { + super(duration); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RefreshInterval(RefreshInterval original) { + super(original); + } + + @Override + public RefreshInterval copy() { + return new RefreshInterval(this); + } +} diff --git a/app/src/main/java/biweekly/property/RelatedTo.java b/app/src/main/java/biweekly/property/RelatedTo.java new file mode 100644 index 0000000000..49e8c21ced --- /dev/null +++ b/app/src/main/java/biweekly/property/RelatedTo.java @@ -0,0 +1,95 @@ +package biweekly.property; + +import biweekly.parameter.RelationshipType; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a relationship between the component that this property belongs to + * and another component. + *

+ *

+ * Code samples: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * RelatedTo relatedTo = new RelatedTo("uid-value");
+ * event.addRelatedTo(relatedTo);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.115-6 + * @see RFC 2445 + * p.109-10 + * @see vCal 1.0 p.33-4 + */ +public class RelatedTo extends TextProperty { + /** + * Creates a related-to property. + * @param uid the value of the {@link Uid} property of the component that + * this property is referencing + */ + public RelatedTo(String uid) { + super(uid); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RelatedTo(RelatedTo original) { + super(original); + } + + /** + * Gets the relationship type. + * @return the relationship type (e.g. "child") or null if not set + * @see RFC 5545 + * p.25 + */ + public RelationshipType getRelationshipType() { + return parameters.getRelationshipType(); + } + + /** + * Sets the relationship type. + * @param relationshipType the relationship type (e.g. "child") or null to + * remove + * @see RFC 5545 + * p.25 + */ + public void setRelationshipType(RelationshipType relationshipType) { + parameters.setRelationshipType(relationshipType); + } + + @Override + public RelatedTo copy() { + return new RelatedTo(this); + } +} diff --git a/app/src/main/java/biweekly/property/Repeat.java b/app/src/main/java/biweekly/property/Repeat.java new file mode 100644 index 0000000000..766a9e8379 --- /dev/null +++ b/app/src/main/java/biweekly/property/Repeat.java @@ -0,0 +1,71 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the number of times an alarm should be repeated after its initial + * trigger. Used in conjunction with {@link DurationProperty}, which defines the + * length of the pause between repeats. + *

+ *

+ * Code sample: + *

+ * + *
+ * //repeat 5 more times after the first time
+ * VAlarm alarm = ...;
+ * alarm.setRepeat(5);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.133 + * @see RFC 2445 + * p.126-7 + */ +public class Repeat extends IntegerProperty { + /** + * Creates a repeat property. + * @param count the number of times to repeat the alarm (e.g. "2" to repeat + * it two more times after it was initially triggered, for a total of three + * times) + */ + public Repeat(Integer count) { + super(count); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Repeat(Repeat original) { + super(original); + } + + @Override + public Repeat copy() { + return new Repeat(this); + } +} diff --git a/app/src/main/java/biweekly/property/RequestStatus.java b/app/src/main/java/biweekly/property/RequestStatus.java new file mode 100644 index 0000000000..42259bf7ba --- /dev/null +++ b/app/src/main/java/biweekly/property/RequestStatus.java @@ -0,0 +1,217 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Represents a response to a scheduling request. + *

+ *

+ * This property must have a status code defined. The iCalendar specification + * defines the following status code families. However, these can have different + * meanings depending upon the type of scheduling request system being used + * (such as iTIP). + *

+ *
    + *
  • 1.x - The request has been received, but is still being processed. + *
  • + *
  • 2.x - The request was processed successfully.
  • + *
  • 3.x - There is a client-side problem with the request (such as + * some incorrect syntax).
  • + *
  • 4.x - A scheduling error occurred on the server that prevented the + * request from being processed.
  • + *
+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * RequestStatus requestStatus = new RequestStatus("2.0");
+ * requestStatus.setDescription("Success");
+ * event.setRequestStatus(requestStatus);
+ * 
+ * @author Michael Angstadt + * @see RFC 5546 + * Section 3.6 + * @see RFC 5545 + * p.141-3 + * @see RFC 2445 + * p.134-6 + */ +public class RequestStatus extends ICalProperty { + private String statusCode, description, exceptionText; + + /** + * Creates a request status property. + * @param statusCode the status code (e.g. "1.1.3") + */ + public RequestStatus(String statusCode) { + setStatusCode(statusCode); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public RequestStatus(RequestStatus original) { + super(original); + statusCode = original.statusCode; + description = original.description; + exceptionText = original.exceptionText; + } + + /** + * Gets the status code. The following status code families are defined: + *
    + *
  • 1.x - The request has been received, but is still being + * processed.
  • + *
  • 2.x - The request was processed successfully.
  • + *
  • 3.x - There is a client-side problem with the request (such as + * some incorrect syntax).
  • + *
  • 4.x - A server-side error occurred.
  • + *
+ * @return the status code (e.g. "1.1.3") + */ + public String getStatusCode() { + return statusCode; + } + + /** + * Sets a status code. The following status code families are defined: + *
    + *
  • 1.x - The request has been received, but is still being + * processed.
  • + *
  • 2.x - The request was processed successfully.
  • + *
  • 3.x - There is a client-side problem with the request (such as + * some incorrect syntax).
  • + *
  • 4.x - A server-side error occurred.
  • + *
+ * @param statusCode the status code (e.g. "1.1.3") + */ + public void setStatusCode(String statusCode) { + this.statusCode = statusCode; + } + + /** + * Gets the human-readable description of the status. + * @return the description (e.g. "Success") or null if not set + */ + public String getDescription() { + return description; + } + + /** + * Sets a human-readable description of the status. + * @param description the description (e.g. "Success") or null to remove + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets any additional data related to the response. + * @return the additional data or null if not set + */ + public String getExceptionText() { + return exceptionText; + } + + /** + * Sets any additional data related to the response. + * @param exceptionText the additional data or null to remove + */ + public void setExceptionText(String exceptionText) { + this.exceptionText = exceptionText; + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (statusCode == null) { + warnings.add(new ValidationWarning(36)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("statusCode", statusCode); + values.put("description", description); + values.put("exceptionText", exceptionText); + return values; + } + + @Override + public RequestStatus copy() { + return new RequestStatus(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((exceptionText == null) ? 0 : exceptionText.hashCode()); + result = prime * result + ((statusCode == null) ? 0 : statusCode.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + RequestStatus other = (RequestStatus) obj; + if (description == null) { + if (other.description != null) return false; + } else if (!description.equals(other.description)) return false; + if (exceptionText == null) { + if (other.exceptionText != null) return false; + } else if (!exceptionText.equals(other.exceptionText)) return false; + if (statusCode == null) { + if (other.statusCode != null) return false; + } else if (!statusCode.equals(other.statusCode)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Resources.java b/app/src/main/java/biweekly/property/Resources.java new file mode 100644 index 0000000000..78886d7030 --- /dev/null +++ b/app/src/main/java/biweekly/property/Resources.java @@ -0,0 +1,108 @@ +package biweekly.property; + +import java.util.List; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a list of physical resources that are needed for an event or to-do + * task (for example, a "projector" or "DVD player"). + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Resources resources = new Resources("projector", "DVD player");
+ * event.addResources(resources);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.91 + * @see RFC 2445 p.87-8 + * @see vCal 1.0 p.34-5 + */ +public class Resources extends ListProperty { + /** + * Creates a new resources property. + */ + public Resources() { + super(); + } + + /** + * Creates a new resources property. + * @param values the values to initialize the property with (e.g. "easel", + * "projector") + */ + public Resources(String... values) { + super(values); + } + + /** + * Creates a new resources property. + * @param values the values to initialize the property with (e.g. "easel", + * "projector") + */ + public Resources(List values) { + super(values); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Resources(Resources original) { + super(original); + } + + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Resources copy() { + return new Resources(this); + } +} diff --git a/app/src/main/java/biweekly/property/Sequence.java b/app/src/main/java/biweekly/property/Sequence.java new file mode 100644 index 0000000000..ea3d9a169f --- /dev/null +++ b/app/src/main/java/biweekly/property/Sequence.java @@ -0,0 +1,82 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a revision number for an event, to-do task, or journal entry. This + * number can be incremented every time a significant change is made to the + * component. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = ...
+ * event.setSequence(2);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.138-9 + * @see RFC 2445 + * p.131-3 + * @see vCal 1.0 p.35 + */ +public class Sequence extends IntegerProperty { + /** + * Creates a sequence property. + * @param sequence the sequence number (e.g. "0" for the initial version, + * "1" for the first revision, etc) + */ + public Sequence(Integer sequence) { + super(sequence); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Sequence(Sequence original) { + super(original); + } + + /** + * Increments the sequence number. + */ + public void increment() { + if (value == null) { + value = 1; + } else { + value++; + } + } + + @Override + public Sequence copy() { + return new Sequence(this); + } +} diff --git a/app/src/main/java/biweekly/property/Source.java b/app/src/main/java/biweekly/property/Source.java new file mode 100644 index 0000000000..1a5e83e4c4 --- /dev/null +++ b/app/src/main/java/biweekly/property/Source.java @@ -0,0 +1,68 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a URI where the calendar data can be refreshed from. + *

+ *

+ * Code sample: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * Source source = new Source("http://example.com/holidays.ics");
+ * ical.setSource(source);
+ * 
+ * @author Michael Angstadt + * @see draft-ietf-calext-extensions-01 + * p.8 + */ +public class Source extends TextProperty { + /** + * Creates a source property. + * @param uri the URI (e.g. "http://example.com/holidays.ics") + */ + public Source(String uri) { + super(uri); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Source(Source original) { + super(original); + } + + @Override + public Source copy() { + return new Source(this); + } +} diff --git a/app/src/main/java/biweekly/property/Status.java b/app/src/main/java/biweekly/property/Status.java new file mode 100644 index 0000000000..498b77c2d2 --- /dev/null +++ b/app/src/main/java/biweekly/property/Status.java @@ -0,0 +1,347 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import biweekly.ICalVersion; +import biweekly.component.VEvent; +import biweekly.component.VJournal; +import biweekly.component.VTodo; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the status of the component that this property belongs to, such as a + * to-do task being "completed". + *

+ * + *

+ * Code sample (creating): + *

+ * + *
+ * VTodo todo = new VTodo();
+ * 
+ * Status status = Status.completed();
+ * todo.setStatus(status);
+ * 
+ * + *

+ * Code sample (retrieving): + *

+ * + *
+ * ICalendar ical = ...
+ * for (VTodo todo : ical.getTodos()) {
+ *   Status status = todo.getStatus();
+ *   if (action.isCompleted()) {
+ *     //...
+ *   } else if (action.isDraft()) {
+ *     //...
+ *   }
+ *   //etc.
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.92-3 + * @see RFC 2445 p.88-9 + * @see vCal 1.0 p.35-6 + */ +public class Status extends EnumProperty { + //2.0 + public static final String CANCELLED = "CANCELLED"; + public static final String DRAFT = "DRAFT"; + public static final String FINAL = "FINAL"; + public static final String IN_PROGRESS = "IN-PROGRESS"; + + //1.0 + public static final String ACCEPTED = "ACCEPTED"; + public static final String DECLINED = "DECLINED"; + public static final String DELEGATED = "DELEGATED"; + public static final String SENT = "SENT"; + + //1.0 and 2.0 + public static final String COMPLETED = "COMPLETED"; + public static final String CONFIRMED = "CONFIRMED"; + public static final String NEEDS_ACTION = "NEEDS-ACTION"; + public static final String TENTATIVE = "TENTATIVE"; + + /** + * Creates a status property. Use of this constructor is discouraged and may + * put the property in an invalid state. Use one of the static factory + * methods instead. + * @param status the status (e.g. "TENTATIVE") + */ + public Status(String status) { + super(status); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Status(Status original) { + super(original); + } + + /** + * Creates a "tentative" status property (only valid for event components). + * @return the property + */ + public static Status tentative() { + return create(TENTATIVE); + } + + /** + * Determines if the status is set to "tentative". + * @return true if it is, false if not + */ + public boolean isTentative() { + return is(TENTATIVE); + } + + /** + * Creates a "confirmed" status property (only valid for event components). + * @return the property + */ + public static Status confirmed() { + return create(CONFIRMED); + } + + /** + * Determines if the status is set to "confirmed". + * @return true if it is, false if not + */ + public boolean isConfirmed() { + return is(CONFIRMED); + } + + /** + * Creates a "cancelled" status property (only valid in iCalendar 2.0 in + * {@link VEvent}, {@link VTodo}, and {@link VJournal} components). + * @return the property + */ + public static Status cancelled() { + return create(CANCELLED); + } + + /** + * Determines if the status is set to "cancelled". + * @return true if it is, false if not + */ + public boolean isCancelled() { + return is(CANCELLED); + } + + /** + * Creates a "needs-action" status property. + * @return the property + */ + public static Status needsAction() { + return create(NEEDS_ACTION); + } + + /** + * Determines if the status is set to "needs-action". + * @return true if it is, false if not + */ + public boolean isNeedsAction() { + return is(NEEDS_ACTION); + } + + /** + * Creates a "completed" status property (only valid in {@link VTodo} + * components). + * @return the property + */ + public static Status completed() { + return create(COMPLETED); + } + + /** + * Determines if the status is set to "completed". + * @return true if it is, false if not + */ + public boolean isCompleted() { + return is(COMPLETED); + } + + /** + * Creates a "in-progress" status property (only valid in iCalendar 2.0 in + * {@link VTodo} components). + * @return the property + */ + public static Status inProgress() { + return create(IN_PROGRESS); + } + + /** + * Determines if the status is set to "in-progress". + * @return true if it is, false if not + */ + public boolean isInProgress() { + return is(IN_PROGRESS); + } + + /** + * Creates a "draft" status property (only valid in iCalendar 2.0 in + * {@link VJournal} components). + * @return the property + */ + public static Status draft() { + return create(DRAFT); + } + + /** + * Determines if the status is set to "draft". + * @return true if it is, false if not + */ + public boolean isDraft() { + return is(DRAFT); + } + + /** + * Creates a "final" status property (only valid in iCalendar 2.0 in + * {@link VJournal} components). + * @return the property + */ + public static Status final_() { + return create(FINAL); + } + + /** + * Determines if the status is set to "final". + * @return true if it is, false if not + */ + public boolean isFinal() { + return is(FINAL); + } + + /** + * Creates an "accepted" status property (only valid in vCal 1.0 in + * {@link VTodo} components). + * @return the property + */ + public static Status accepted() { + return create(ACCEPTED); + } + + /** + * Determines if the status is set to "accepted". + * @return true if it is, false if not + */ + public boolean isAccepted() { + return is(ACCEPTED); + } + + /** + * Creates a "declined" status property (only valid in vCal 1.0). + * @return the property + */ + public static Status declined() { + return create(DECLINED); + } + + /** + * Determines if the status is set to "declined". + * @return true if it is, false if not + */ + public boolean isDeclined() { + return is(DECLINED); + } + + /** + * Creates a "delegated" status property (only valid in vCal 1.0). + * @return the property + */ + public static Status delegated() { + return create(DELEGATED); + } + + /** + * Determines if the status is set to "delegated". + * @return true if it is, false if not + */ + public boolean isDelegated() { + return is(DELEGATED); + } + + /** + * Creates a "sent" status property (only valid in vCal 1.0). + * @return the property + */ + public static Status sent() { + return create(SENT); + } + + /** + * Determines if the status is set to "sent". + * @return true if it is, false if not + */ + public boolean isSent() { + return is(SENT); + } + + public static Status create(String status) { + return new Status(status); + } + + @Override + protected Collection getStandardValues(ICalVersion version) { + switch (version) { + case V1_0: + return Arrays.asList(ACCEPTED, COMPLETED, CONFIRMED, DECLINED, DELEGATED, NEEDS_ACTION, SENT, TENTATIVE); + default: + return Arrays.asList(CANCELLED, COMPLETED, CONFIRMED, DRAFT, FINAL, IN_PROGRESS, NEEDS_ACTION, TENTATIVE); + } + } + + @Override + protected Collection getValueSupportedVersions() { + if (value == null) { + return Collections.emptyList(); + } + + if (isCompleted() || isConfirmed() || isNeedsAction() || isTentative()) { + return Arrays.asList(ICalVersion.values()); + } + if (isCancelled() || isDraft() || isFinal() || isInProgress()) { + return Arrays.asList(ICalVersion.V2_0_DEPRECATED, ICalVersion.V2_0); + } + if (isAccepted() || isDeclined() || isDelegated() || isSent()) { + return Collections.singletonList(ICalVersion.V1_0); + } + + return Collections.emptyList(); + } + + @Override + public Status copy() { + return new Status(this); + } +} diff --git a/app/src/main/java/biweekly/property/Summary.java b/app/src/main/java/biweekly/property/Summary.java new file mode 100644 index 0000000000..4f4b8c5513 --- /dev/null +++ b/app/src/main/java/biweekly/property/Summary.java @@ -0,0 +1,91 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a short, one line summary of the component that this property belongs + * to. The summary should be a more concise version of the text provided by the + * {@link Description} property. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Summary summary = new Summary("Team Meeting");
+ * event.setSummary(summary);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.93-4 + * @see RFC 2445 + * p.89-90 + * @see vCal 1.0 p.36 + */ +public class Summary extends TextProperty { + /** + * Creates a new summary property. + * @param summary the summary + */ + public Summary(String summary) { + super(summary); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Summary(Summary original) { + super(original); + } + + @Override + public String getAltRepresentation() { + return super.getAltRepresentation(); + } + + @Override + public void setAltRepresentation(String uri) { + super.setAltRepresentation(uri); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public Summary copy() { + return new Summary(this); + } +} diff --git a/app/src/main/java/biweekly/property/TextProperty.java b/app/src/main/java/biweekly/property/TextProperty.java new file mode 100644 index 0000000000..80aa8ab95f --- /dev/null +++ b/app/src/main/java/biweekly/property/TextProperty.java @@ -0,0 +1,49 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is a plain-text string. Note that this + * includes both text and URI values. + * @author Michael Angstadt + */ +public class TextProperty extends ValuedProperty { + /** + * Creates a new text property. + * @param value the property's value + */ + public TextProperty(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public TextProperty(TextProperty original) { + super(original); + } +} diff --git a/app/src/main/java/biweekly/property/Timezone.java b/app/src/main/java/biweekly/property/Timezone.java new file mode 100644 index 0000000000..eb8269146d --- /dev/null +++ b/app/src/main/java/biweekly/property/Timezone.java @@ -0,0 +1,74 @@ +package biweekly.property; + +import java.util.List; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. + */ + +/** + * Defines the local timezone. + * @author Michael Angstadt + * @see vCal 1.0 p.24 + */ +public class Timezone extends UtcOffsetProperty { + /** + * Creates a timezone property. + * @param offset the UTC offset + */ + public Timezone(UtcOffset offset) { + super(offset); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Timezone(Timezone original) { + super(original); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + super.validate(components, version, warnings); + + if (version != ICalVersion.V1_0) { + warnings.add(new ValidationWarning(45, version)); + } + } + + @Override + public Timezone copy() { + return new Timezone(this); + } +} diff --git a/app/src/main/java/biweekly/property/TimezoneId.java b/app/src/main/java/biweekly/property/TimezoneId.java new file mode 100644 index 0000000000..91b22899fe --- /dev/null +++ b/app/src/main/java/biweekly/property/TimezoneId.java @@ -0,0 +1,59 @@ +package biweekly.property; + +import biweekly.component.VTimezone; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines a unique identifier for a {@link VTimezone} component. The identifier + * must be unique within the scope of the iCalendar object. + * @author Michael Angstadt + * @see RFC 5545 + * p.102-3 + * @see RFC 2445 p.97-8 + */ +public class TimezoneId extends TextProperty { + /** + * Creates a timezone identifier property. + * @param timezone the timezone identifier + */ + public TimezoneId(String timezone) { + super(timezone); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public TimezoneId(TimezoneId original) { + super(original); + } + + @Override + public TimezoneId copy() { + return new TimezoneId(this); + } +} diff --git a/app/src/main/java/biweekly/property/TimezoneName.java b/app/src/main/java/biweekly/property/TimezoneName.java new file mode 100644 index 0000000000..ae404f2258 --- /dev/null +++ b/app/src/main/java/biweekly/property/TimezoneName.java @@ -0,0 +1,82 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a traditional, human-readable, non-standard name for a timezone + * observance (for example, "Eastern Standard Time" for standard time on the US + * east coast). + *

+ *

+ * Code sample: + *

+ * + *
+ * VTimezone timezone = new VTimezone("East Coast");
+ * 
+ * StandardTime standard = new StandardTime();
+ * standard.setTimezoneName("Eastern Standard Time");
+ * ...
+ * timezone.addStandardTime(standard);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.103-4 + * @see RFC 2445 p.98-9 + */ +public class TimezoneName extends TextProperty { + /** + * Creates a timezone name property. + * @param name the timezone name (e.g. "EST") + */ + public TimezoneName(String name) { + super(name); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public TimezoneName(TimezoneName original) { + super(original); + } + + @Override + public String getLanguage() { + return super.getLanguage(); + } + + @Override + public void setLanguage(String language) { + super.setLanguage(language); + } + + @Override + public TimezoneName copy() { + return new TimezoneName(this); + } +} diff --git a/app/src/main/java/biweekly/property/TimezoneOffsetFrom.java b/app/src/main/java/biweekly/property/TimezoneOffsetFrom.java new file mode 100644 index 0000000000..fd2549537b --- /dev/null +++ b/app/src/main/java/biweekly/property/TimezoneOffsetFrom.java @@ -0,0 +1,73 @@ +package biweekly.property; + +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the timezone offset that was in affect before the timezone observance + * began. + *

+ *

+ * Code sample: + *

+ * + *
+ * VTimezone timezone = ...
+ * StandardTime standard = new StandardTime();
+ * standard.setTimezoneOffsetFrom(-5, 0);
+ * ...
+ * timezone.addStandardTime(standard);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.104-5 + * @see RFC 2445 + * p.99-100 + */ +public class TimezoneOffsetFrom extends UtcOffsetProperty { + /** + * Creates a new timezone offset from property. + * @param offset the UTC offset + */ + public TimezoneOffsetFrom(UtcOffset offset) { + super(offset); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public TimezoneOffsetFrom(TimezoneOffsetFrom original) { + super(original); + } + + @Override + public TimezoneOffsetFrom copy() { + return new TimezoneOffsetFrom(this); + } +} diff --git a/app/src/main/java/biweekly/property/TimezoneOffsetTo.java b/app/src/main/java/biweekly/property/TimezoneOffsetTo.java new file mode 100644 index 0000000000..e72d74efe3 --- /dev/null +++ b/app/src/main/java/biweekly/property/TimezoneOffsetTo.java @@ -0,0 +1,72 @@ +package biweekly.property; + +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the timezone offset that a timezone observance uses. + *

+ *

+ * Code sample: + *

+ * + *
+ * VTimezone timezone = ...
+ * StandardTime standard = new StandardTime();
+ * standard.setTimezoneOffsetTo(-4, 0);
+ * ...
+ * timezone.addStandardTime(standard);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.105-6 + * @see RFC 2445 + * p.100-1 + */ +public class TimezoneOffsetTo extends UtcOffsetProperty { + /** + * Creates a new timezone offset to property. + * @param offset the UTC offset + */ + public TimezoneOffsetTo(UtcOffset offset) { + super(offset); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public TimezoneOffsetTo(TimezoneOffsetTo original) { + super(original); + } + + @Override + public TimezoneOffsetTo copy() { + return new TimezoneOffsetTo(this); + } +} diff --git a/app/src/main/java/biweekly/property/TimezoneUrl.java b/app/src/main/java/biweekly/property/TimezoneUrl.java new file mode 100644 index 0000000000..10eacfa216 --- /dev/null +++ b/app/src/main/java/biweekly/property/TimezoneUrl.java @@ -0,0 +1,67 @@ +package biweekly.property; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a URL that points to an iCalendar object that contains further + * information on a timezone. + *

+ *

+ * Code sample: + *

+ * + *
+ * VTimezone timezone = ...
+ * timezone.setTimezoneUrl("http://example.com/tz.ics");
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 p.106 + * @see RFC 2445 p.101 + */ +public class TimezoneUrl extends TextProperty { + /** + * Creates a timezone URL property. + * @param url the timezone URL (e.g. + * "http://example.com/America-New_York.ics") + */ + public TimezoneUrl(String url) { + super(url); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public TimezoneUrl(TimezoneUrl original) { + super(original); + } + + @Override + public TimezoneUrl copy() { + return new TimezoneUrl(this); + } +} diff --git a/app/src/main/java/biweekly/property/Transparency.java b/app/src/main/java/biweekly/property/Transparency.java new file mode 100644 index 0000000000..44dd0eb849 --- /dev/null +++ b/app/src/main/java/biweekly/property/Transparency.java @@ -0,0 +1,141 @@ +package biweekly.property; + +import java.util.Arrays; +import java.util.Collection; + +import biweekly.ICalVersion; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines whether an event is visible to free/busy time searches or not. If an + * event does not have this property, the event should be considered opaque + * (visible) to searches. + *

+ *

+ * Code sample (creating): + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Transparency transp = Transparency.opaque();
+ * event.setTransparency(transp);
+ * 
+ * event.setTransparency(true); //hidden from searches
+ * event.setTransparency(false); //visible to searches
+ * 
+ * + *

+ * Code sample (retrieving): + *

+ * + *
+ * ICalendar ical = ...
+ * for (VEvent event : ical.getEvents()) {
+ *   Transparency transp = event.getTransparency();
+ *   if (transp.isOpaque()) {
+ *     //...
+ *   } else if (transp.isTransparent()) {
+ *     //...
+ *   }
+ * }
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.101-2 + * @see RFC 2445 p.96-7 + * @see vCal 1.0 p.36-7 + */ +public class Transparency extends EnumProperty { + public static final String OPAQUE = "OPAQUE"; + public static final String TRANSPARENT = "TRANSPARENT"; + + /** + * Creates a new transparency property. + * @param value the value + */ + public Transparency(String value) { + super(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Transparency(Transparency original) { + super(original); + } + + /** + * Creates a property that marks the event as being visible to free/busy + * time searches. + * @return the property + */ + public static Transparency opaque() { + return create(OPAQUE); + } + + /** + * Determines if the event is visible to free/busy time searches. + * @return true if it's visible, false if not + */ + public boolean isOpaque() { + return is(OPAQUE); + } + + /** + * Creates a property that marks the event as being hidden from free/busy + * time searches. + * @return the property + */ + public static Transparency transparent() { + return create(TRANSPARENT); + } + + /** + * Determines if the event is hidden from free/busy time searches. + * @return true if it's hidden, false if not + */ + public boolean isTransparent() { + return is(TRANSPARENT); + } + + private static Transparency create(String value) { + return new Transparency(value); + } + + @Override + protected Collection getStandardValues(ICalVersion version) { + return Arrays.asList(OPAQUE, TRANSPARENT); + } + + @Override + public Transparency copy() { + return new Transparency(this); + } +} diff --git a/app/src/main/java/biweekly/property/Trigger.java b/app/src/main/java/biweekly/property/Trigger.java new file mode 100644 index 0000000000..6aae5cccb1 --- /dev/null +++ b/app/src/main/java/biweekly/property/Trigger.java @@ -0,0 +1,194 @@ +package biweekly.property; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.parameter.Related; +import biweekly.util.Duration; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines when to trigger an alarm. + *

+ *

+ * Code sample: + *

+ * + *
+ * //15 minutes before the start time
+ * Duration duration = Duration.builder().prior(true).minutes(15).build();
+ * Trigger trigger = new Trigger(duration, Related.START);
+ * VAlarm alarm = VAlarm.display(trigger, "Meeting in 15 minutes");
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.133-6 + * @see RFC 2445 + * p.127-9 + */ +public class Trigger extends ICalProperty { + private Duration duration; + private Date date; + + /** + * Creates a trigger property. + * @param duration the relative time + * @param related the date-time field that the duration is relative to + */ + public Trigger(Duration duration, Related related) { + setDuration(duration, related); + } + + /** + * Creates a trigger property. + * @param date the date-time the alarm will trigger. + */ + public Trigger(Date date) { + setDate(date); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Trigger(Trigger original) { + super(original); + date = (original.date == null) ? null : new Date(original.date.getTime()); + duration = original.duration; + } + + /** + * Gets the relative time at which the alarm will trigger. + * @return the relative time or null if an absolute time is set + */ + public Duration getDuration() { + return duration; + } + + /** + * Sets a relative time at which the alarm will trigger. + * @param duration the relative time + * @param related the date-time field that the duration is relative to + */ + public void setDuration(Duration duration, Related related) { + this.date = null; + this.duration = duration; + setRelated(related); + } + + /** + * Gets the date-time that the alarm will trigger. + * @return the date-time or null if a relative duration is set + */ + public Date getDate() { + return date; + } + + /** + * Sets the date-time that the alarm will trigger. + * @param date the date-time the alarm will trigger. + */ + public void setDate(Date date) { + this.date = date; + this.duration = null; + setRelated(null); + } + + /** + * Gets the date-time field that the duration is relative to. + * @return the field or null if not set + * @see RFC 5545 + * p.24 + */ + public Related getRelated() { + return parameters.getRelated(); + } + + /** + * Sets the date-time field that the duration is relative to. + * @param related the field or null to remove + * @see RFC 5545 + * p.24 + */ + public void setRelated(Related related) { + parameters.setRelated(related); + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (duration == null && date == null) { + warnings.add(new ValidationWarning(33)); + } + + Related related = getRelated(); + if (duration != null && related == null) { + warnings.add(new ValidationWarning(10)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("duration", duration); + values.put("date", date); + return values; + } + + @Override + public Trigger copy() { + return new Trigger(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((date == null) ? 0 : date.hashCode()); + result = prime * result + ((duration == null) ? 0 : duration.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Trigger other = (Trigger) obj; + if (date == null) { + if (other.date != null) return false; + } else if (!date.equals(other.date)) return false; + if (duration == null) { + if (other.duration != null) return false; + } else if (!duration.equals(other.duration)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Uid.java b/app/src/main/java/biweekly/property/Uid.java new file mode 100644 index 0000000000..243b0d966e --- /dev/null +++ b/app/src/main/java/biweekly/property/Uid.java @@ -0,0 +1,86 @@ +package biweekly.property; + +import java.util.UUID; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a unique identifier for a component. Note that all components that + * require UID properties are automatically given a random one on creation. + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Uid uid = new Uid("19970610T172345Z-AF23B2@example.com");
+ * event.setUid(uid);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.117-8 + * @see RFC 2445 + * p.111-2 + * @see vCal 1.0 p.37 + * @see draft-ietf-calext-extensions-01 + * p.6 + */ +public class Uid extends TextProperty { + /** + * Creates a UID property. + * @param uid the UID (can be anything) + */ + public Uid(String uid) { + super(uid); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Uid(Uid original) { + super(original); + } + + /** + * Creates a UID property that contains a {@link UUID universally unique + * identifier}. + * @return the property + */ + public static Uid random() { + String uuid = UUID.randomUUID().toString(); + return new Uid(uuid); + } + + @Override + public Uid copy() { + return new Uid(this); + } +} diff --git a/app/src/main/java/biweekly/property/Url.java b/app/src/main/java/biweekly/property/Url.java new file mode 100644 index 0000000000..5317485b9a --- /dev/null +++ b/app/src/main/java/biweekly/property/Url.java @@ -0,0 +1,80 @@ +package biweekly.property; + +import biweekly.ICalendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines a website that contains additional information about a component. + *

+ *

+ * If defined in the top-level {@link ICalendar} component, this property + * defines the location of a more dynamic, alternate representation of the + * calendar (for example, a Google Calendar). + *

+ *

+ * Code sample: + *

+ * + *
+ * VEvent event = new VEvent();
+ * 
+ * Url url = new Url("http://example.com");
+ * event.setUrl(url);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.116-7 + * @see RFC 2445 + * p.110-1 + * @see vCal 1.0 p.37 + * @see draft-ietf-calext-extensions-01 + * p.6 + */ +public class Url extends TextProperty { + /** + * Creates a URL property. + * @param url the URL (e.g. "http://example.com/resource.ics") + */ + public Url(String url) { + super(url); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Url(Url original) { + super(original); + } + + @Override + public Url copy() { + return new Url(this); + } +} diff --git a/app/src/main/java/biweekly/property/UtcOffsetProperty.java b/app/src/main/java/biweekly/property/UtcOffsetProperty.java new file mode 100644 index 0000000000..654e54971a --- /dev/null +++ b/app/src/main/java/biweekly/property/UtcOffsetProperty.java @@ -0,0 +1,46 @@ +package biweekly.property; + +import biweekly.util.UtcOffset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose value is a timezone offset. + * @author Michael Angstadt + */ +public class UtcOffsetProperty extends ValuedProperty { + public UtcOffsetProperty(UtcOffset offset) { + super(offset); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public UtcOffsetProperty(UtcOffsetProperty original) { + super(original); + } +} diff --git a/app/src/main/java/biweekly/property/VCalAlarmProperty.java b/app/src/main/java/biweekly/property/VCalAlarmProperty.java new file mode 100644 index 0000000000..fe7b808c28 --- /dev/null +++ b/app/src/main/java/biweekly/property/VCalAlarmProperty.java @@ -0,0 +1,128 @@ +package biweekly.property; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import biweekly.component.VAlarm; +import biweekly.util.Duration; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines an alarm property that is part of the vCalendar (1.0) standard (such + * as {@link AudioAlarm}). + *

+ *

+ * Classes that extend this class are used internally by this library for + * parsing purposes. If you are creating a new iCalendar object and need to + * define an alarm, it is recommended that you use the {@link VAlarm} component + * to create a new alarm. + *

+ * @author Michael Angstadt + * @see vCal 1.0 + */ +public class VCalAlarmProperty extends ICalProperty { + protected Date start; + protected Duration snooze; + protected Integer repeat; + + public VCalAlarmProperty() { + //empty + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public VCalAlarmProperty(VCalAlarmProperty original) { + super(original); + start = new Date(original.start.getTime()); + snooze = original.snooze; + repeat = original.repeat; + } + + public Date getStart() { + return start; + } + + public void setStart(Date start) { + this.start = start; + } + + public Duration getSnooze() { + return snooze; + } + + public void setSnooze(Duration snooze) { + this.snooze = snooze; + } + + public Integer getRepeat() { + return repeat; + } + + public void setRepeat(Integer repeat) { + this.repeat = repeat; + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("start", start); + values.put("snooze", snooze); + values.put("repeat", repeat); + return values; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((repeat == null) ? 0 : repeat.hashCode()); + result = prime * result + ((snooze == null) ? 0 : snooze.hashCode()); + result = prime * result + ((start == null) ? 0 : start.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + VCalAlarmProperty other = (VCalAlarmProperty) obj; + if (repeat == null) { + if (other.repeat != null) return false; + } else if (!repeat.equals(other.repeat)) return false; + if (snooze == null) { + if (other.snooze != null) return false; + } else if (!snooze.equals(other.snooze)) return false; + if (start == null) { + if (other.start != null) return false; + } else if (!start.equals(other.start)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/ValuedProperty.java b/app/src/main/java/biweekly/property/ValuedProperty.java new file mode 100644 index 0000000000..3603568cc8 --- /dev/null +++ b/app/src/main/java/biweekly/property/ValuedProperty.java @@ -0,0 +1,169 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a property whose data model consists of a single Java object (such + * as a String). + * @author Michael Angstadt + * @param the value class (e.g. String) + */ +public class ValuedProperty extends ICalProperty { + protected T value; + + /** + * Creates a new valued property. + * @param value the property's value + */ + public ValuedProperty(T value) { + setValue(value); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public ValuedProperty(ValuedProperty original) { + super(original); + value = original.value; + } + + /** + * Gets the value of this property. + * @return the value + */ + public T getValue() { + return value; + } + + /** + * Sets the value of this property. + * @param value the value + */ + public void setValue(T value) { + this.value = value; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (value == null) { + warnings.add(new ValidationWarning(26)); + } + } + + /** + * Utility method that gets the value of a {@link ValuedProperty} object. + * @param property the property object (may be null) + * @param the value class + * @return the property value (may be null), or null if the property object + * itself is null + */ + public static T getValue(ValuedProperty property) { + return (property == null) ? null : property.getValue(); + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("value", value); + return values; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((value == null) ? 0 : valueHashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + + /* + * This cast will not fail because each property's Class objects are + * checked for equality in super.equals(). + */ + @SuppressWarnings("unchecked") + ValuedProperty other = (ValuedProperty) obj; + + if (value == null) { + if (other.value != null) return false; + } else if (!valueEquals(other.value)) return false; + return true; + } + + /** + *

+ * Calculates the hash code of this property's value. + *

+ *

+ * This method is meant by to overridden by child classes whose value's hash + * code cannot be calculated by just invoking {@code hashCode()}. For + * example, a property whose value is case insensitive. The default + * implementation of this method calls {@code value.hashCode()}. + *

+ *

+ * This method is only invoked when this property's value is not null. + *

+ * @return the value's hash code + */ + protected int valueHashCode() { + return value.hashCode(); + } + + /** + *

+ * Compares this property's value with another property's value for + * equality. + *

+ *

+ * This method is meant by to overridden by child classes when their value's + * equality cannot be calculated by just invoking {@code equals()}. For + * example, a property whose value is case insensitive. The default + * implementation of this method calls {@code value.equals(otherValue)}. + *

+ *

+ * This method is only invoked when this property's value is not null. + *

+ * @param otherValue the other property's value + * @return true if this property's value is equal to the other property's + * value, false if not + */ + protected boolean valueEquals(T otherValue) { + return value.equals(otherValue); + } +} diff --git a/app/src/main/java/biweekly/property/Version.java b/app/src/main/java/biweekly/property/Version.java new file mode 100644 index 0000000000..5353f395e0 --- /dev/null +++ b/app/src/main/java/biweekly/property/Version.java @@ -0,0 +1,238 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.VersionNumber; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Defines the min/max iCalendar versions a consumer must support in order to + * successfully parse the iCalendar object. + *

+ *

+ * Code sample: + *

+ * + *
+ * ICalendar ical = new ICalendar();
+ * 
+ * //all ICalendar objects are given a VERSION property on creation
+ * ical.getVersion(); //"2.0"
+ * 
+ * //get the default iCal version
+ * Version version = Version.v2_0();
+ * ical.setVersion(version);
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.79-80 + * @see RFC 2445 p.76-7 + * @see vCal 1.0 p.24 + */ +public class Version extends ICalProperty { + public static final VersionNumber VCAL = new VersionNumber(ICalVersion.V1_0.getVersion()); + public static final VersionNumber ICAL = new VersionNumber(ICalVersion.V2_0.getVersion()); + + private VersionNumber minVersion, maxVersion; + + /** + * Creates a new version property. + * @param version the version that a consumer must support in order to + * successfully parse the iCalendar object + */ + public Version(ICalVersion version) { + this((version == null) ? null : version.getVersion()); + } + + /** + * Creates a new version property. + * @param version the version that a consumer must support in order to + * successfully parse the iCalendar object + * @throws IllegalArgumentException if the version string is invalid + */ + public Version(String version) { + this(null, version); + } + + /** + * Creates a new version property. + * @param minVersion the minimum version that a consumer must support in + * order to successfully parse the iCalendar object + * @param maxVersion the maximum version that a consumer must support in + * order to successfully parse the iCalendar object + * @throws IllegalArgumentException if one of the versions strings are + * invalid + */ + public Version(String minVersion, String maxVersion) { + this((minVersion == null) ? null : new VersionNumber(minVersion), (maxVersion == null) ? null : new VersionNumber(maxVersion)); + } + + private Version(VersionNumber minVersion, VersionNumber maxVersion) { + this.minVersion = minVersion; + this.maxVersion = maxVersion; + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Version(Version original) { + super(original); + minVersion = original.minVersion; + maxVersion = original.maxVersion; + } + + /** + * Creates a version property that is set to the older vCalendar version + * (1.0). + * @return the property instance + */ + public static Version v1_0() { + return new Version(ICalVersion.V1_0); + } + + /** + * Creates a version property that is set to the latest iCalendar version + * (2.0). + * @return the property instance + */ + public static Version v2_0() { + return new Version(ICalVersion.V2_0); + } + + /** + * Determines if this property is set to the older vCalendar version. + * @return true if the version is "1.0", false if not + */ + public boolean isV1_0() { + return VCAL.equals(maxVersion); + } + + /** + * Determines if this property is set to the latest iCalendar version. + * @return true if the version is "2.0", false if not + */ + public boolean isV2_0() { + return ICAL.equals(maxVersion); + } + + /** + * Gets the minimum version that a consumer must support in order to + * successfully parse the iCalendar object. + * @return the minimum version or null if not set + */ + public VersionNumber getMinVersion() { + return minVersion; + } + + /** + * Sets the minimum version that a consumer must support in order to + * successfully parse the iCalendar object. + * @param minVersion the minimum version or null to remove + */ + public void setMinVersion(VersionNumber minVersion) { + this.minVersion = minVersion; + } + + /** + * Gets the maximum version that a consumer must support in order to + * successfully parse the iCalendar object. + * @return the maximum version or null if not set + */ + public VersionNumber getMaxVersion() { + return maxVersion; + } + + /** + * Sets the maximum version that a consumer must support in order to + * successfully parse the iCalendar object. + * @param maxVersion the maximum version (this field is required) + */ + public void setMaxVersion(VersionNumber maxVersion) { + this.maxVersion = maxVersion; + } + + /** + * Converts this property's value to an {@link ICalVersion} enum. + * @return the {@link ICalVersion} enum or null if it couldn't be converted + */ + public ICalVersion toICalVersion() { + if (minVersion == null && maxVersion != null) { + return ICalVersion.get(maxVersion.toString()); + } + return null; + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (maxVersion == null) { + warnings.add(new ValidationWarning(35)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("minVersion", minVersion); + values.put("maxVersion", maxVersion); + return values; + } + + @Override + public Version copy() { + return new Version(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((maxVersion == null) ? 0 : maxVersion.hashCode()); + result = prime * result + ((minVersion == null) ? 0 : minVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + Version other = (Version) obj; + if (maxVersion == null) { + if (other.maxVersion != null) return false; + } else if (!maxVersion.equals(other.maxVersion)) return false; + if (minVersion == null) { + if (other.minVersion != null) return false; + } else if (!minVersion.equals(other.minVersion)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/property/Xml.java b/app/src/main/java/biweekly/property/Xml.java new file mode 100644 index 0000000000..5b088d73c7 --- /dev/null +++ b/app/src/main/java/biweekly/property/Xml.java @@ -0,0 +1,124 @@ +package biweekly.property; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import biweekly.ICalVersion; +import biweekly.ValidationWarning; +import biweekly.component.ICalComponent; +import biweekly.util.XmlUtils; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Stores a property that was parsed from an xCal document (XML-encoded + * iCalendar object) whose XML namespace was not part of the xCal XML namespace. + * @author Michael Angstadt + * @see RFC 6321 p.17-8 + */ +public class Xml extends ValuedProperty { + /** + * Creates an XML property. + * @param xml the XML to use as the property's value + * @throws SAXException if the XML cannot be parsed + */ + public Xml(String xml) throws SAXException { + this((xml == null) ? null : XmlUtils.toDocument(xml)); + } + + /** + * Creates an XML property. + * @param element the XML element to use as the property's value (the + * element is imported into an empty {@link Document} object) + */ + public Xml(Element element) { + this((element == null) ? null : XmlUtils.createDocument()); + if (element != null) { + Node imported = value.importNode(element, true); + value.appendChild(imported); + } + } + + /** + * Creates an XML property. + * @param document the XML document to use as the property's value + */ + public Xml(Document document) { + super(document); + } + + /** + * Copy constructor. + * @param original the property to make a copy of + */ + public Xml(Xml original) { + super(original); + if (original.value != null) { + value = XmlUtils.createDocument(); + Element root = original.value.getDocumentElement(); + if (root != null) { + Node node = value.importNode(root, true); + value.appendChild(node); + } + } + } + + @Override + protected void validate(List components, ICalVersion version, List warnings) { + if (value == null) { + warnings.add(new ValidationWarning(26)); + } + } + + @Override + protected Map toStringValues() { + Map values = new LinkedHashMap(); + values.put("value", (value == null) ? "null" : XmlUtils.toString(value)); + return values; + } + + @Override + public Xml copy() { + return new Xml(this); + } + + @Override + protected int valueHashCode() { + return XmlUtils.toString(value).hashCode(); + } + + @Override + protected boolean valueEquals(Document otherValue) { + if (otherValue == null) return false; + return XmlUtils.toString(value).equals(XmlUtils.toString(otherValue)); + } +} diff --git a/app/src/main/java/biweekly/property/package-info.java b/app/src/main/java/biweekly/property/package-info.java new file mode 100644 index 0000000000..aaa76dc56c --- /dev/null +++ b/app/src/main/java/biweekly/property/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains DTO classes for each property. + */ +package biweekly.property; \ No newline at end of file diff --git a/app/src/main/java/biweekly/util/ByDay.java b/app/src/main/java/biweekly/util/ByDay.java new file mode 100644 index 0000000000..04ae667b4a --- /dev/null +++ b/app/src/main/java/biweekly/util/ByDay.java @@ -0,0 +1,104 @@ +package biweekly.util; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + *

+ * Represents a specific day or all days in a month or year. + *

+ *

+ * Examples: + *

+ *
    + *
  • {@code new ByDay(3, DayOfWeek.MONDAY)} - The third Monday in + * the month/year.
  • + *
  • {@code new ByDay(-1, DayOfWeek.MONDAY)} - The last Monday in the + * month/year.
  • + *
  • {@code new ByDay(DayOfWeek.MONDAY)} - Every Monday in the month/year. + *
  • + *
+ * @author Michael Angstadt + */ +public class ByDay { + private final Integer num; + private final DayOfWeek day; + + /** + * Creates a BYDAY rule that represents all days in the month/year. + * @param day the day of the week (cannot be null) + */ + public ByDay(DayOfWeek day) { + this(null, day); + } + + /** + * Creates a BYDAY rule. + * @param num the number (e.g. 3 for "third Sunday", cannot be zero) + * @param day the day of the week (cannot be null) + */ + public ByDay(Integer num, DayOfWeek day) { + this.num = num; + this.day = day; + } + + /** + * Gets the number. + * @return the number (can be null) + */ + public Integer getNum() { + return num; + } + + /** + * Gets the day of the week. + * @return the day of the week + */ + public DayOfWeek getDay() { + return day; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((day == null) ? 0 : day.hashCode()); + result = prime * result + ((num == null) ? 0 : num.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + ByDay other = (ByDay) obj; + if (day != other.day) return false; + if (num == null) { + if (other.num != null) return false; + } else if (!num.equals(other.num)) return false; + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/util/CaseClasses.java b/app/src/main/java/biweekly/util/CaseClasses.java new file mode 100644 index 0000000000..e69e1027af --- /dev/null +++ b/app/src/main/java/biweekly/util/CaseClasses.java @@ -0,0 +1,162 @@ +package biweekly.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Manages objects that are like enums in that they are constant, but unlike + * enums in that new instances can be created during runtime. This class ensures + * that all instances of a class are unique, so they can be safely compared + * using "==" (provided their constructors are private). It mimics the + * "case class" feature in Scala. + * @author Michael Angstadt + * + * @param the class + * @param the value that the class holds (e.g. String) + */ +public abstract class CaseClasses { + protected final Class clazz; + protected volatile Collection preDefined = null; + protected Collection runtimeDefined = null; + + /** + * Creates a new case class collection. + * @param clazz the case class + */ + public CaseClasses(Class clazz) { + this.clazz = clazz; + } + + /** + * Creates a new instance of the case class. + * @param value the value to give the instance + * @return the new instance + */ + protected abstract T create(V value); + + /** + * Determines if a value is associated with a case object. + * @param object the object + * @param value the value + * @return true if it matches, false if not + */ + protected abstract boolean matches(T object, V value); + + /** + * Searches for a case object by value, only looking at the case class' + * static constants (does not include runtime-defined objects). + * @param value the value + * @return the object or null if one wasn't found + */ + public T find(V value) { + checkInit(); + + for (T obj : preDefined) { + if (matches(obj, value)) { + return obj; + } + } + return null; + } + + /** + * Searches for a case object by value, creating a new object if one cannot + * be found. + * @param value the value + * @return the object + */ + public T get(V value) { + T found = find(value); + if (found != null) { + return found; + } + + synchronized (runtimeDefined) { + for (T obj : runtimeDefined) { + if (matches(obj, value)) { + return obj; + } + } + + T created = create(value); + runtimeDefined.add(created); + return created; + } + } + + /** + * Gets all the static constants of the case class. + * @return all static constants + */ + public Collection all() { + checkInit(); + return preDefined; + } + + private void checkInit() { + if (preDefined == null) { + synchronized (this) { + //"double check idiom" (Bloch p.283) + if (preDefined == null) { + init(); + } + } + } + } + + private void init() { + Collection preDefined = new ArrayList(); + for (Field field : clazz.getFields()) { + int modifiers = field.getModifiers(); + //@formatter:off + if (Modifier.isStatic(modifiers) && + Modifier.isPublic(modifiers) && + field.getDeclaringClass() == clazz && + field.getType() == clazz) { + //@formatter:on + try { + Object obj = field.get(null); + if (obj != null) { + T c = clazz.cast(obj); + preDefined.add(c); + } + } catch (Exception ex) { + //reflection error + //should never be thrown because we check for "public static" and the correct type + throw new RuntimeException(ex); + } + } + } + + runtimeDefined = new ArrayList(0); + this.preDefined = Collections.unmodifiableCollection(preDefined); + } +} diff --git a/app/src/main/java/biweekly/util/DataUri.java b/app/src/main/java/biweekly/util/DataUri.java new file mode 100644 index 0000000000..cc471395c4 --- /dev/null +++ b/app/src/main/java/biweekly/util/DataUri.java @@ -0,0 +1,265 @@ +package biweekly.util; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import biweekly.Messages; +import biweekly.util.org.apache.commons.codec.binary.Base64; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Represents a data URI. + *

+ *

+ * Example: {@code data:image/jpeg;base64,[base64 string]} + *

+ * @author Michael Angstadt + */ +public final class DataUri { + private final byte[] data; + private final String text; + private final String contentType; + + /** + * Creates a data URI. + * @param contentType the content type of the data (e.g. "image/png") + * @param data the data + */ + public DataUri(String contentType, byte[] data) { + this(contentType, data, null); + } + + /** + * Creates a data URI. + * @param contentType the content type of the text (e.g. "text/html") + * @param text the text + */ + public DataUri(String contentType, String text) { + this(contentType, null, text); + } + + /** + * Creates a data URI with a content type of "text/plain". + * @param text the text + */ + public DataUri(String text) { + this("text/plain", text); + } + + /** + * Copies a data URI. + * @param original the data URI to copy + */ + public DataUri(DataUri original) { + this(original.contentType, (original.data == null) ? null : original.data.clone(), original.text); + } + + private DataUri(String contentType, byte[] data, String text) { + this.contentType = (contentType == null) ? "" : contentType.toLowerCase(); + this.data = data; + this.text = text; + } + + /** + * Parses a data URI string. + * @param uri the URI string (e.g. "data:image/jpeg;base64,[base64 string]") + * @return the parsed data URI + * @throws IllegalArgumentException if the string is not a valid data URI or + * it cannot be parsed + */ + public static DataUri parse(String uri) { + //Syntax: data:[][;charset=][;base64], + + String scheme = "data:"; + if (uri.length() < scheme.length() || !uri.substring(0, scheme.length()).equalsIgnoreCase(scheme)) { + //not a data URI + throw Messages.INSTANCE.getIllegalArgumentException(22); + } + + String contentType = null; + String charset = null; + boolean base64 = false; + String dataStr = null; + int tokenStart = scheme.length(); + for (int i = scheme.length(); i < uri.length(); i++) { + char c = uri.charAt(i); + + if (c == ';') { + String token = uri.substring(tokenStart, i); + if (contentType == null) { + contentType = token.toLowerCase(); + } else { + String cs = StringUtils.afterPrefixIgnoreCase(token, "charset="); + if (cs != null) { + charset = cs; + } else if ("base64".equalsIgnoreCase(token)) { + base64 = true; + } + } + tokenStart = i + 1; + continue; + } + + if (c == ',') { + String token = uri.substring(tokenStart, i); + if (contentType == null) { + contentType = token.toLowerCase(); + } else { + String cs = StringUtils.afterPrefixIgnoreCase(token, "charset="); + if (cs != null) { + charset = cs; + } else if ("base64".equalsIgnoreCase(token)) { + base64 = true; + } + } + + dataStr = uri.substring(i + 1); + break; + } + } + + if (dataStr == null) { + throw Messages.INSTANCE.getIllegalArgumentException(23); + } + + String text = null; + byte[] data = null; + if (base64) { + dataStr = dataStr.replaceAll("\\s", ""); + data = Base64.decodeBase64(dataStr); + if (charset != null) { + try { + text = new String(data, charset); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(24, charset), e); + } + data = null; + } + } else { + text = dataStr; + } + + return new DataUri(contentType, data, text); + } + + /** + * Gets the binary data. + * @return the binary data or null if the value was text + */ + public byte[] getData() { + return data; + } + + /** + * Gets the content type. + * @return the content type (e.g. "image/png") + */ + public String getContentType() { + return contentType; + } + + /** + * Gets the text value. + * @return the text value or null if the value was binary + */ + public String getText() { + return text; + } + + /** + * Creates a data URI string. + * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]") + */ + @Override + public String toString() { + return toString(null); + } + + /** + * Creates a data URI string. + * @param charset only applicable if the data URI's value is text. Defines + * the character set to encode the text in, or null not to specify a + * character set + * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]") + * @throws IllegalArgumentException if the given character set is not + * supported by this JVM + */ + public String toString(String charset) { + StringBuilder sb = new StringBuilder(); + sb.append("data:"); + sb.append(contentType); + + if (data != null) { + sb.append(";base64,"); + sb.append(Base64.encodeBase64String(data)); + } else if (text != null) { + if (charset == null) { + sb.append(',').append(text); + } else { + byte[] data; + try { + data = text.getBytes(charset); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(25, charset), e); + } + + sb.append(";charset=").append(charset); + sb.append(";base64,"); + sb.append(Base64.encodeBase64String(data)); + } + } else { + sb.append(','); + } + + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + contentType.hashCode(); + result = prime * result + Arrays.hashCode(data); + result = prime * result + ((text == null) ? 0 : text.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + DataUri other = (DataUri) obj; + if (!contentType.equals(other.contentType)) return false; + if (!Arrays.equals(data, other.data)) return false; + if (text == null) { + if (other.text != null) return false; + } else if (!text.equals(other.text)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/util/DateTimeComponents.java b/app/src/main/java/biweekly/util/DateTimeComponents.java new file mode 100644 index 0000000000..481ff74b59 --- /dev/null +++ b/app/src/main/java/biweekly/util/DateTimeComponents.java @@ -0,0 +1,426 @@ +package biweekly.util; + +import java.io.Serializable; +import java.text.NumberFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import biweekly.Messages; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Contains the raw components of a date-time value. + *

+ *

+ * Examples: + *

+ * + *
+ * //July 22, 2013 at 17:25
+ * DateTimeComponents components = new DateTimeComponents(2013, 07, 22, 17, 25, 0, false);
+ * 
+ * //parsing a date string (accepts basic and extended formats)
+ * DateTimeComponents components = DateTimeComponents.parse("20130722T172500");
+ * 
+ * //converting to date string
+ * DateTimeComponents components = new DateTimeComponents(2013, 07, 22, 17, 25, 0, false);
+ * String str = components.toString(true); //"2013-07-22T17:25:00"
+ * 
+ * //converting to a Date object
+ * DateTimeComponents components = new DateTimeComponents(2013, 07, 22, 17, 25, 0, false);
+ * Date date = components.toDate();
+ * 
+ * 
+ * @author Michael Angstadt + */ +public final class DateTimeComponents implements Comparable, Serializable { + private static final long serialVersionUID = 7668029303206402368L; + private static final Pattern regex = Pattern.compile("^(\\d{4})-?(\\d{2})-?(\\d{2})(T(\\d{2}):?(\\d{2}):?(\\d{2})(Z?))?.*"); + private final int year, month, date, hour, minute, second; + private final boolean hasTime, utc; + + /** + * Parses the components out of a date-time string. + * @param dateString the date-time string (basic and extended formats are + * supported, e.g. "20130331T020000" or "2013-03-31T02:00:00") + * @return the parsed components + * @throws IllegalArgumentException if the date string cannot be parsed + */ + public static DateTimeComponents parse(String dateString) { + return parse(dateString, null); + } + + /** + * Parses the components out of a date-time string. + * @param dateString the date-time string (basic and extended formats are + * supported, e.g. "20130331T020000" or "2013-03-31T02:00:00") + * @param hasTime true to force the value to be parsed as a date-time value, + * false to force the value to be parsed as a date value, null to parse the + * value however it is + * @return the parsed components + * @throws IllegalArgumentException if the date string cannot be parsed + */ + public static DateTimeComponents parse(String dateString, Boolean hasTime) { + Matcher m = regex.matcher(dateString); + if (!m.find()) { + throw Messages.INSTANCE.getIllegalArgumentException(19, dateString); + } + + int i = 1; + int year = Integer.parseInt(m.group(i++)); + int month = Integer.parseInt(m.group(i++)); + int date = Integer.parseInt(m.group(i++)); + + i++; //skip + + String hourStr = m.group(i++); + if (hasTime == null) { + hasTime = (hourStr != null); + } + if (!hasTime) { + return new DateTimeComponents(year, month, date); + } + + int hour = (hourStr == null) ? 0 : Integer.parseInt(hourStr); + + String minuteStr = m.group(i++); + int minute = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr); + + String secondStr = m.group(i++); + int second = (secondStr == null) ? 0 : Integer.parseInt(secondStr); + + boolean utc = "Z".equals(m.group(i++)); + + return new DateTimeComponents(year, month, date, hour, minute, second, utc); + } + + /** + * Copies an existing DateTimeComponents object. + * @param original the object to copy from + */ + public DateTimeComponents(DateTimeComponents original) { + this(original, null, null, null, null, null, null, null); + } + + /** + * Copies an existing DateTimeComponents object. + * @param original the object to copy from + * @param year the new year value or null not to change + * @param month the new month value or null not to change + * @param date the new date value or null not to change + * @param hour the new hour value or null not to change + * @param minute the new minute value or null not to change + * @param second the new second value or null not to change + * @param utc true if the time is in UTC, false if not, or null not to + * change + */ + public DateTimeComponents(DateTimeComponents original, Integer year, Integer month, Integer date, Integer hour, Integer minute, Integer second, Boolean utc) { + //@formatter:off + this( + (year == null) ? original.year : year, + (month == null) ? original.month : month, + (date == null) ? original.date : date, + (hour == null) ? original.hour : hour, + (minute == null) ? original.minute : minute, + (second == null) ? original.second : second, + (utc == null) ? original.utc : utc + ); + //@formatter:on + } + + /** + * Creates a set of date components. + * @param year the year (e.g. "2013") + * @param month the month (e.g. "1" for January) + * @param date the date of the month (e.g. "15") + */ + public DateTimeComponents(int year, int month, int date) { + this(year, month, date, 0, 0, 0, false, false); + } + + /** + * Creates a set of date-time components. + * @param year the year (e.g. "2013") + * @param month the month (e.g. "1" for January) + * @param date the date of the month (e.g. "15") + * @param hour the hour (e.g. "13") + * @param minute the minute + * @param second the second + * @param utc true if the time is in UTC, false if not + */ + public DateTimeComponents(int year, int month, int date, int hour, int minute, int second, boolean utc) { + this(year, month, date, hour, minute, second, utc, true); + } + + private DateTimeComponents(int year, int month, int date, int hour, int minute, int second, boolean utc, boolean hasTime) { + this.year = year; + this.month = month; + this.date = date; + this.hour = hour; + this.minute = minute; + this.second = second; + this.utc = utc; + this.hasTime = hasTime; + } + + /** + * Creates a set of date-time components in the local timezone from a + * {@link Date} object. + * @param date the date object + */ + public DateTimeComponents(Date date) { + this(date, TimeZone.getDefault()); + } + + /** + * Creates a set of date-time components from a {@link Date} object. + * @param date the date object + * @param timezone the timezone the date-time components will be in + */ + public DateTimeComponents(Date date, TimeZone timezone) { + Calendar cal = Calendar.getInstance(timezone); + cal.setTime(date); + + year = cal.get(Calendar.YEAR); + month = cal.get(Calendar.MONTH) + 1; + this.date = cal.get(Calendar.DATE); + hour = cal.get(Calendar.HOUR_OF_DAY); + minute = cal.get(Calendar.MINUTE); + second = cal.get(Calendar.SECOND); + utc = false; + hasTime = true; + } + + /** + * Gets the year component. + * @return the year + */ + public int getYear() { + return year; + } + + /** + * Gets the month component. + * @return the month (e.g. "1" for January) + */ + public int getMonth() { + return month; + } + + /** + * Gets the date component + * @return the date + */ + public int getDate() { + return date; + } + + /** + * Gets whether these components contain a time component + * @return true if it has a time component, false if it's strictly a date + */ + public boolean hasTime() { + return hasTime; + } + + /** + * Gets the hour component + * @return the hour + */ + public int getHour() { + return hour; + } + + /** + * Gets the minute component. + * @return the minute + */ + public int getMinute() { + return minute; + } + + /** + * Gets the second component. + * @return the second + */ + public int getSecond() { + return second; + } + + /** + * Gets whether the time is in UTC or not + * @return true if the time is in UTC, false if not + */ + public boolean isUtc() { + return utc; + } + + /** + * Converts the date-time components to a string using "basic" format. + * @return the date string + */ + @Override + public String toString() { + return toString(true, false); + } + + /** + * Converts the date-time components to a string. + * @param includeTime true to include the time portion, false not to + * @param extended true to use extended format, false to use basic + * @return the date string + */ + public String toString(boolean includeTime, boolean extended) { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.ENGLISH); + nf.setMinimumIntegerDigits(2); + nf.setMaximumIntegerDigits(2); + String dash = extended ? "-" : ""; + String colon = extended ? ":" : ""; + String z = utc ? "Z" : ""; + + StringBuilder sb = new StringBuilder(); + sb.append(year).append(dash).append(nf.format(month)).append(dash).append(nf.format(date)); + if (includeTime) { + sb.append("T").append(nf.format(hour)).append(colon).append(nf.format(minute)).append(colon).append(nf.format(second)).append(z); + } + return sb.toString(); + } + + /** + * Converts the date-time components to a {@link Date} object. + * @return the date object + */ + public Date toDate() { + TimeZone timezone = utc ? TimeZone.getTimeZone("UTC") : TimeZone.getDefault(); + return toDate(timezone); + } + + /** + * Converts the date-time components to a {@link Date} object. + * @param timezone the timezone that the date-time components are assumed to + * be in + * @return the date object + */ + public Date toDate(TimeZone timezone) { + return toDate(Calendar.getInstance(timezone)); + } + + /** + * Converts the date-time components to a {@link Date} object. + * @param c the calendar object to use + * @return the date object + */ + public Date toDate(Calendar c) { + c.clear(); + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month - 1); + c.set(Calendar.DATE, date); + c.set(Calendar.HOUR_OF_DAY, hour); + c.set(Calendar.MINUTE, minute); + c.set(Calendar.SECOND, second); + return c.getTime(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + date; + result = prime * result + (hasTime ? 1231 : 1237); + result = prime * result + hour; + result = prime * result + minute; + result = prime * result + month; + result = prime * result + second; + result = prime * result + (utc ? 1231 : 1237); + result = prime * result + year; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + DateTimeComponents other = (DateTimeComponents) obj; + if (date != other.date) return false; + if (hasTime != other.hasTime) return false; + if (hour != other.hour) return false; + if (minute != other.minute) return false; + if (month != other.month) return false; + if (second != other.second) return false; + if (utc != other.utc) return false; + if (year != other.year) return false; + return true; + } + + public int compareTo(DateTimeComponents that) { + int c = this.year - that.year; + if (c != 0) { + return c; + } + + c = this.month - that.month; + if (c != 0) { + return c; + } + + c = this.date - that.date; + if (c != 0) { + return c; + } + + c = this.hour - that.hour; + if (c != 0) { + return c; + } + + c = this.minute - that.minute; + if (c != 0) { + return c; + } + + c = this.second - that.second; + if (c != 0) { + return c; + } + + return 0; + } + + public boolean before(DateTimeComponents that) { + return this.compareTo(that) < 0; + } + + public boolean after(DateTimeComponents that) { + return this.compareTo(that) > 0; + } +} diff --git a/app/src/main/java/biweekly/util/DayOfWeek.java b/app/src/main/java/biweekly/util/DayOfWeek.java new file mode 100644 index 0000000000..9f1c901cb6 --- /dev/null +++ b/app/src/main/java/biweekly/util/DayOfWeek.java @@ -0,0 +1,88 @@ +package biweekly.util; + +import java.util.Calendar; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents each of the seven days of the week. + * @author Michael Angstadt + */ +public enum DayOfWeek { + /* + * These constants are defined in the order that the days of the week are + * arranged on a calendar. Do not rearrange them. Other parts of the code + * base rely on this ordering (i.e. this class's ordinal() and values() + * methods are used). + */ + //@formatter:off + SUNDAY("SU", Calendar.SUNDAY), + MONDAY("MO", Calendar.MONDAY), + TUESDAY("TU", Calendar.TUESDAY), + WEDNESDAY("WE", Calendar.WEDNESDAY), + THURSDAY("TH", Calendar.THURSDAY), + FRIDAY("FR", Calendar.FRIDAY), + SATURDAY("SA", Calendar.SATURDAY); + //@formatter:on + + private final String abbr; + private final int calendarConstant; + + DayOfWeek(String abbr, int calendarConstant) { + this.abbr = abbr; + this.calendarConstant = calendarConstant; + } + + /** + * Gets the day's abbreviation. + * @return the abbreviation (e.g. "MO" for Monday) + */ + public String getAbbr() { + return abbr; + } + + /** + * Gets the integer constant the {@link Calendar} class uses for this day. + * @return the constant + */ + public int getCalendarConstant() { + return calendarConstant; + } + + /** + * Gets a day by its abbreviation. + * @param abbr the abbreviation (case-insensitive, e.g. "MO" for Monday) + * @return the day or null if not found + */ + public static DayOfWeek valueOfAbbr(String abbr) { + for (DayOfWeek day : values()) { + if (day.abbr.equalsIgnoreCase(abbr)) { + return day; + } + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/biweekly/util/Duration.java b/app/src/main/java/biweekly/util/Duration.java new file mode 100644 index 0000000000..035c4274d6 --- /dev/null +++ b/app/src/main/java/biweekly/util/Duration.java @@ -0,0 +1,509 @@ +package biweekly.util; + +import java.util.Calendar; +import java.util.Date; + +import biweekly.Messages; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Represents a period of time (for example, "2 hours and 30 minutes"). + *

+ *

+ * This class is immutable. Use the {@link #builder} method to construct a new + * instance, or the {@link #parse} method to parse a duration string. + *

+ * + *

+ * Examples: + *

+ * + *
+ * Duration duration = Duration.builder().hours(2).minutes(30).build();
+ * Duration duration = Duration.parse("PT2H30M");
+ * 
+ * //add a duration value to a Date
+ * Date start = ...
+ * Date end = duration.add(start);
+ * 
+ * @author Michael Angstadt + */ +public final class Duration { + private final Integer weeks, days, hours, minutes, seconds; + private final boolean prior; + + private Duration(Builder b) { + weeks = b.weeks; + days = b.days; + hours = b.hours; + minutes = b.minutes; + seconds = b.seconds; + prior = b.prior; + } + + /** + * Parses a duration string. + * @param value the duration string (e.g. "P30DT10H") + * @return the parsed duration + * @throws IllegalArgumentException if the duration string is invalid + */ + public static Duration parse(String value) { + /* + * Implementation note: Regular expressions are not used to improve + * performance. + */ + + if (value.isEmpty()) { + throw parseError(value); + } + + int index = 0; + char first = value.charAt(index); + boolean prior = (first == '-'); + if (first == '-' || first == '+') { + index++; + } + + if (value.charAt(index) != 'P') { + throw parseError(value); + } + + Builder builder = new Builder(); + builder.prior(prior); + + StringBuilder buffer = new StringBuilder(); + for (int i = index + 1; i < value.length(); i++) { + char c = value.charAt(i); + + if (c == 'T') { + /* + * A "T" character is supposed to immediately precede the time + * component value(s). It is required by the syntax, but not + * really necessary. Ignore it. + */ + continue; + } + + if (c >= '0' && c <= '9') { + buffer.append(c); + continue; + } + + if (buffer.length() == 0) { + throw parseError(value); + } + + Integer num = Integer.valueOf(buffer.toString()); + buffer.setLength(0); + + switch (c) { + case 'W': + builder.weeks(num); + break; + case 'D': + builder.days(num); + break; + case 'H': + builder.hours(num); + break; + case 'M': + builder.minutes(num); + break; + case 'S': + builder.seconds(num); + break; + default: + throw parseError(value); + } + } + + return builder.build(); + } + + private static IllegalArgumentException parseError(String value) { + return Messages.INSTANCE.getIllegalArgumentException(20, value); + } + + /** + * Builds a duration based on the difference between two dates. + * @param start the start date + * @param end the end date + * @return the duration + */ + public static Duration diff(Date start, Date end) { + return fromMillis(end.getTime() - start.getTime()); + } + + /** + * Builds a duration from a number of milliseconds. + * @param milliseconds the number of milliseconds + * @return the duration + */ + public static Duration fromMillis(long milliseconds) { + Duration.Builder builder = builder(); + + if (milliseconds < 0) { + builder.prior(true); + milliseconds *= -1; + } + + int seconds = (int) (milliseconds / 1000); + + int weeks = seconds / (60 * 60 * 24 * 7); + if (weeks > 0) { + builder.weeks(weeks); + } + seconds %= 60 * 60 * 24 * 7; + + int days = seconds / (60 * 60 * 24); + if (days > 0) { + builder.days(days); + } + seconds %= 60 * 60 * 24; + + int hours = seconds / (60 * 60); + if (hours > 0) { + builder.hours(hours); + } + seconds %= 60 * 60; + + int minutes = seconds / (60); + if (minutes > 0) { + builder.minutes(minutes); + } + seconds %= 60; + + if (seconds > 0) { + builder.seconds(seconds); + } + + return builder.build(); + } + + /** + * Creates a builder object for constructing new instances of this class. + * @return the builder object + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets whether the duration is negative. + * @return true if it's negative, false if not + */ + public boolean isPrior() { + return prior; + } + + /** + * Gets the number of weeks. + * @return the number of weeks or null if not set + */ + public Integer getWeeks() { + return weeks; + } + + /** + * Gets the number of days. + * @return the number of days or null if not set + */ + public Integer getDays() { + return days; + } + + /** + * Gets the number of hours. + * @return the number of hours or null if not set + */ + public Integer getHours() { + return hours; + } + + /** + * Gets the number of minutes. + * @return the number of minutes or null if not set + */ + public Integer getMinutes() { + return minutes; + } + + /** + * Gets the number of seconds. + * @return the number of seconds or null if not set + */ + public Integer getSeconds() { + return seconds; + } + + /** + * Adds this duration value to a {@link Date} object. + * @param date the date to add to + * @return the new date value + */ + public Date add(Date date) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + + if (weeks != null) { + int weeks = this.weeks * (prior ? -1 : 1); + c.add(Calendar.DATE, weeks * 7); + } + if (days != null) { + int days = this.days * (prior ? -1 : 1); + c.add(Calendar.DATE, days); + } + if (hours != null) { + int hours = this.hours * (prior ? -1 : 1); + c.add(Calendar.HOUR_OF_DAY, hours); + } + if (minutes != null) { + int minutes = this.minutes * (prior ? -1 : 1); + c.add(Calendar.MINUTE, minutes); + } + if (seconds != null) { + int seconds = this.seconds * (prior ? -1 : 1); + c.add(Calendar.SECOND, seconds); + } + + return c.getTime(); + } + + /** + * Converts the duration value to milliseconds. + * @return the duration value in milliseconds (will be negative if + * {@link #isPrior} is true) + */ + public long toMillis() { + long totalSeconds = 0; + + if (weeks != null) { + totalSeconds += 60L * 60 * 24 * 7 * weeks; + } + if (days != null) { + totalSeconds += 60L * 60 * 24 * days; + } + if (hours != null) { + totalSeconds += 60L * 60 * hours; + } + if (minutes != null) { + totalSeconds += 60L * minutes; + } + if (seconds != null) { + totalSeconds += seconds; + } + if (prior) { + totalSeconds *= -1; + } + + return totalSeconds * 1000; + } + + /** + * Determines if any time components are present. + * @return true if the duration has at least one time component, false if + * not + */ + public boolean hasTime() { + return hours != null || minutes != null || seconds != null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((days == null) ? 0 : days.hashCode()); + result = prime * result + ((hours == null) ? 0 : hours.hashCode()); + result = prime * result + ((minutes == null) ? 0 : minutes.hashCode()); + result = prime * result + (prior ? 1231 : 1237); + result = prime * result + ((seconds == null) ? 0 : seconds.hashCode()); + result = prime * result + ((weeks == null) ? 0 : weeks.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Duration other = (Duration) obj; + if (days == null) { + if (other.days != null) return false; + } else if (!days.equals(other.days)) return false; + if (hours == null) { + if (other.hours != null) return false; + } else if (!hours.equals(other.hours)) return false; + if (minutes == null) { + if (other.minutes != null) return false; + } else if (!minutes.equals(other.minutes)) return false; + if (prior != other.prior) return false; + if (seconds == null) { + if (other.seconds != null) return false; + } else if (!seconds.equals(other.seconds)) return false; + if (weeks == null) { + if (other.weeks != null) return false; + } else if (!weeks.equals(other.weeks)) return false; + return true; + } + + /** + * Converts the duration to its string representation. + * @return the string representation (e.g. "P4DT1H" for "4 days and 1 hour") + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (prior) { + sb.append('-'); + } + sb.append('P'); + + if (weeks != null) { + sb.append(weeks).append('W'); + } + + if (days != null) { + sb.append(days).append('D'); + } + + if (hasTime()) { + sb.append('T'); + + if (hours != null) { + sb.append(hours).append('H'); + } + + if (minutes != null) { + sb.append(minutes).append('M'); + } + + if (seconds != null) { + sb.append(seconds).append('S'); + } + } + + return sb.toString(); + } + + /** + * Builds {@link Duration} objects. + */ + public static class Builder { + private Integer weeks, days, hours, minutes, seconds; + private boolean prior = false; + + /** + * Creates a new {@link Duration} builder. + */ + public Builder() { + //empty + } + + /** + * Creates a new {@link Duration} builder. + * @param source the object to copy from + */ + public Builder(Duration source) { + weeks = source.weeks; + days = source.days; + hours = source.hours; + minutes = source.minutes; + seconds = source.seconds; + prior = source.prior; + } + + /** + * Sets the number of weeks. + * @param weeks the number of weeks + * @return this + */ + public Builder weeks(Integer weeks) { + this.weeks = weeks; + return this; + } + + /** + * Sets the number of days + * @param days the number of days + * @return this + */ + public Builder days(Integer days) { + this.days = days; + return this; + } + + /** + * Sets the number of hours + * @param hours the number of hours + * @return this + */ + public Builder hours(Integer hours) { + this.hours = hours; + return this; + } + + /** + * Sets the number of minutes + * @param minutes the number of minutes + * @return this + */ + public Builder minutes(Integer minutes) { + this.minutes = minutes; + return this; + } + + /** + * Sets the number of seconds. + * @param seconds the number of seconds + * @return this + */ + public Builder seconds(Integer seconds) { + this.seconds = seconds; + return this; + } + + /** + * Sets whether the duration should be negative. + * @param prior true to be negative, false not to be + * @return this + */ + public Builder prior(boolean prior) { + this.prior = prior; + return this; + } + + /** + * Builds the final {@link Duration} object. + * @return the object + */ + public Duration build() { + return new Duration(this); + } + } +} diff --git a/app/src/main/java/biweekly/util/Frequency.java b/app/src/main/java/biweekly/util/Frequency.java new file mode 100644 index 0000000000..f73cdc184a --- /dev/null +++ b/app/src/main/java/biweekly/util/Frequency.java @@ -0,0 +1,35 @@ +package biweekly.util; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + * Represents the frequency at which a recurrence rule repeats itself. + * @author Michael Angstadt + */ +public enum Frequency { + //in order of increasing length + SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY +} diff --git a/app/src/main/java/biweekly/util/Gobble.java b/app/src/main/java/biweekly/util/Gobble.java new file mode 100644 index 0000000000..881e047387 --- /dev/null +++ b/app/src/main/java/biweekly/util/Gobble.java @@ -0,0 +1,152 @@ +package biweekly.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Gets the entire contents of an input stream or a file. + * @author Michael Angstadt + */ +public class Gobble { + private final File file; + private final InputStream in; + private final Reader reader; + + /** + * Gets the contents of a file. + * @param file the file + */ + public Gobble(File file) { + this(file, null, null); + } + + /** + * Gets the contents of an input stream. + * @param in the input stream + */ + public Gobble(InputStream in) { + this(null, in, null); + } + + /** + * Gets the contents of a reader. + * @param reader the reader + */ + public Gobble(Reader reader) { + this(null, null, reader); + } + + private Gobble(File file, InputStream in, Reader reader) { + this.file = file; + this.in = in; + this.reader = reader; + } + + /** + * Gets the stream contents as a string. If something other than a + * {@link Reader} was passed into this class's constructor, this method + * decodes the stream data using the system's default character encoding. + * @return the string + * @throws IOException if there was a problem reading from the stream + */ + public String asString() throws IOException { + return asString(Charset.defaultCharset().name()); + } + + /** + * Gets the stream contents as a string. + * @param charset the character set to decode the stream data with (this + * parameter is ignored if a {@link Reader} was passed into this class's + * constructor) + * @return the string + * @throws IOException if there was a problem reading from the stream + */ + public String asString(String charset) throws IOException { + Reader reader = buildReader(charset); + return consumeReader(reader); + } + + /** + * Gets the stream contents as a byte array. + * @return the byte array + * @throws IOException if there was a problem reading from the stream + * @throws IllegalStateException if a {@link Reader} object was passed into + * this class's constructor + */ + public byte[] asByteArray() throws IOException { + if (reader != null) { + throw new IllegalStateException("Cannot get raw bytes from a Reader object."); + } + + InputStream in = buildInputStream(); + return consumeInputStream(in); + } + + private Reader buildReader(String charset) throws IOException { + return (reader == null) ? new InputStreamReader(buildInputStream(), charset) : reader; + } + + private InputStream buildInputStream() throws IOException { + return (in == null) ? new BufferedInputStream(new FileInputStream(file)) : in; + } + + private String consumeReader(Reader reader) throws IOException { + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[4096]; + int read; + try { + while ((read = reader.read(buffer)) != -1) { + sb.append(buffer, 0, read); + } + } finally { + reader.close(); + } + return sb.toString(); + } + + private byte[] consumeInputStream(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int read; + try { + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } finally { + in.close(); + } + return out.toByteArray(); + } +} diff --git a/app/src/main/java/biweekly/util/Google2445Utils.java b/app/src/main/java/biweekly/util/Google2445Utils.java new file mode 100644 index 0000000000..9aefa61353 --- /dev/null +++ b/app/src/main/java/biweekly/util/Google2445Utils.java @@ -0,0 +1,393 @@ +package biweekly.util; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.TimeZone; + +import biweekly.component.ICalComponent; +import biweekly.property.DateStart; +import biweekly.property.ExceptionDates; +import biweekly.property.ExceptionRule; +import biweekly.property.RecurrenceDates; +import biweekly.property.RecurrenceRule; +import biweekly.property.ValuedProperty; +import biweekly.util.com.google.ical.compat.javautil.DateIterator; +import biweekly.util.com.google.ical.compat.javautil.DateIteratorFactory; +import biweekly.util.com.google.ical.iter.RecurrenceIterable; +import biweekly.util.com.google.ical.iter.RecurrenceIterator; +import biweekly.util.com.google.ical.iter.RecurrenceIteratorFactory; +import biweekly.util.com.google.ical.values.DateTimeValue; +import biweekly.util.com.google.ical.values.DateTimeValueImpl; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Contains utility methods related to the google-rfc-2445 project. + * @author Michael Angstadt + * @see google-rfc-2445 + */ +public final class Google2445Utils { + /** + *

+ * Converts an {@link ICalDate} object to a google-rfc-2445 + * {@link DateValue} object using the {@link DateTimeComponents raw date + * components} of the {@link ICalDate}. + *

+ *

+ * If the {@link ICalDate} object does not have raw date components, then + * the returned {@link DateValue} object will represent the {@link ICalDate} + * in the local timezone. + *

+ * @param date the date object + * @return the google-rfc-2445 object + */ + public static DateValue convertFromRawComponents(ICalDate date) { + DateTimeComponents raw = date.getRawComponents(); + if (raw == null) { + raw = new DateTimeComponents(date); + } + + return convert(raw); + } + + /** + * Converts a {@link DateTimeComponents} object to a google-rfc-2445 + * {@link DateValue} object. + * @param components the date time components object + * @return the google-rfc-2445 object + */ + public static DateValue convert(DateTimeComponents components) { + if (components.hasTime()) { + //@formatter:off + return new DateTimeValueImpl( + components.getYear(), + components.getMonth(), + components.getDate(), + components.getHour(), + components.getMinute(), + components.getSecond() + ); + //@formatter:on + } + + //@formatter:off + return new DateValueImpl( + components.getYear(), + components.getMonth(), + components.getDate() + ); + //@formatter:on + } + + /** + * Converts an {@link ICalDate} object to a google-rfc-2445 + * {@link DateValue} object. + * @param date the date object + * @param timezone the timezone the returned object will be in + * @return the google-rfc-2445 object + */ + public static DateValue convert(ICalDate date, TimeZone timezone) { + Calendar c = Calendar.getInstance(timezone); + c.setTime(date); + + /* + * Do not create a "DateValueImpl" object if "date.hasTime() == false". + * This interferes with any recurrence iterators the object is passed + * into. + * + * See: https://github.com/mangstadt/biweekly/issues/47 + */ + + //@formatter:off + return new DateTimeValueImpl( + c.get(Calendar.YEAR), + c.get(Calendar.MONTH)+1, + c.get(Calendar.DATE), + c.get(Calendar.HOUR_OF_DAY), + c.get(Calendar.MINUTE), + c.get(Calendar.SECOND) + ); + //@formatter:on + } + + /** + * Converts an {@link ICalDate} object to a google-rfc-2445 + * {@link DateValue} object. The returned object will be in UTC. + * @param date the date object + * @return the google-rfc-2445 object (in UTC) + */ + public static DateValue convertUtc(ICalDate date) { + return convert(date, utc()); + } + + /** + * Converts a google-rfc-2445 {@link DateValue} object to a {@link ICalDate} + * object. + * @param date the date value object + * @param timezone the timezone the date value is in + * @return the converted object + */ + public static ICalDate convert(DateValue date, TimeZone timezone) { + Calendar c = Calendar.getInstance(timezone); + c.clear(); + c.set(Calendar.YEAR, date.year()); + c.set(Calendar.MONTH, date.month() - 1); + c.set(Calendar.DATE, date.day()); + + boolean hasTime = (date instanceof DateTimeValue); + if (hasTime) { + DateTimeValue dateTime = (DateTimeValue) date; + c.set(Calendar.HOUR_OF_DAY, dateTime.hour()); + c.set(Calendar.MINUTE, dateTime.minute()); + c.set(Calendar.SECOND, dateTime.second()); + } + + return new ICalDate(c.getTime(), hasTime); + } + + /** + * Converts a google-rfc-2445 {@link DateValue} object to a {@link ICalDate} + * object. It is assumed that the given {@link DateValue} object is in UTC. + * @param date the date value object (in UTC) + * @return the converted object + */ + public static ICalDate convertUtc(DateValue date) { + return convert(date, utc()); + } + + /** + * Creates a recurrence iterator based on the given recurrence rule. + * @param recurrence the recurrence rule + * @param start the start date + * @param timezone the timezone to iterate in. This is needed in order to + * account for when the iterator passes over a daylight savings boundary. + * @return the recurrence iterator + */ + public static RecurrenceIterator createRecurrenceIterator(Recurrence recurrence, ICalDate start, TimeZone timezone) { + DateValue startValue = convert(start, timezone); + return RecurrenceIteratorFactory.createRecurrenceIterator(recurrence, startValue, timezone); + } + + /** + * Creates a recurrence iterator based on the given recurrence rule. + * @param recurrence the recurrence rule + * @param start the start date + * @param timezone the timezone to iterate in. This is needed in order to + * account for when the iterator passes over a daylight savings boundary. + * @return the recurrence iterator + */ + public static RecurrenceIterable createRecurrenceIterable(Recurrence recurrence, ICalDate start, TimeZone timezone) { + DateValue startValue = convert(start, timezone); + return RecurrenceIteratorFactory.createRecurrenceIterable(recurrence, startValue, timezone); + } + + /** + *

+ * Creates an iterator that computes the dates defined by the + * {@link RecurrenceRule} and {@link RecurrenceDates} properties (if + * present), and excludes those dates which are defined by the + * {@link ExceptionRule} and {@link ExceptionDates} properties (if present). + *

+ *

+ * In order for {@link RecurrenceRule} and {@link ExceptionRule} properties + * to be included in this iterator, a {@link DateStart} property must be + * defined. + *

+ *

+ * {@link Period} values in {@link RecurrenceDates} properties are not + * supported and are ignored. + *

+ * @param component the component + * @param timezone the timezone to iterate in. This is needed in order to + * adjust for when the iterator passes over a daylight savings boundary. + * This parameter is ignored if the start date of the given component does + * not have a time component. + * @return the iterator + */ + public static DateIterator getDateIterator(ICalComponent component, TimeZone timezone) { + DateStart dtstart = component.getProperty(DateStart.class); + ICalDate start = ValuedProperty.getValue(dtstart); + + /* + * If the start date is just a date and does not have a time component, + * then the default timezone must be used, because this is the timezone + * biweekly used to parse the date. + */ + if (start != null && !start.hasTime()) { + timezone = TimeZone.getDefault(); + } + + /////////////INCLUDE///////////// + + List include = new ArrayList(); + + if (start != null) { + for (RecurrenceRule rrule : component.getProperties(RecurrenceRule.class)) { + Recurrence recurrence = ValuedProperty.getValue(rrule); + if (recurrence != null) { + include.add(createRecurrenceIterator(recurrence, start, timezone)); + } + } + } + + List allDates = new ArrayList(); + for (RecurrenceDates rdate : component.getProperties(RecurrenceDates.class)) { + allDates.addAll(rdate.getDates()); + } + if (!allDates.isEmpty()) { + include.add(new ICalDateRecurrenceIterator(allDates)); + } + + if (include.isEmpty()) { + if (start == null) { + return new EmptyDateIterator(); + } + include.add(new ICalDateRecurrenceIterator(Collections.singletonList(start))); + } + + /////////////EXCLUDE///////////// + + List exclude = new ArrayList(); + + if (start != null) { + for (ExceptionRule exrule : component.getProperties(ExceptionRule.class)) { + Recurrence recurrence = ValuedProperty.getValue(exrule); + if (recurrence != null) { + exclude.add(createRecurrenceIterator(recurrence, start, timezone)); + } + } + } + + allDates = new ArrayList(); + for (ExceptionDates exdate : component.getProperties(ExceptionDates.class)) { + allDates.addAll(exdate.getValues()); + } + if (!allDates.isEmpty()) { + exclude.add(new ICalDateRecurrenceIterator(allDates)); + } + + /////////////JOIN///////////// + + RecurrenceIterator includeJoined = join(include); + if (exclude.isEmpty()) { + return DateIteratorFactory.createDateIterator(includeJoined); + } + + RecurrenceIterator excludeJoined = join(exclude); + RecurrenceIterator iterator = RecurrenceIteratorFactory.except(includeJoined, excludeJoined); + return DateIteratorFactory.createDateIterator(iterator); + } + + /** + * Creates a single {@link RecurrenceIterator} that is a union of the given + * iterators. + * @param iterators the iterators + * @return the union of the given iterators + * @see RecurrenceIteratorFactory#join + */ + private static RecurrenceIterator join(List iterators) { + if (iterators.size() == 1) { + return iterators.get(0); + } + RecurrenceIterator first = iterators.get(0); + List rest = iterators.subList(1, iterators.size()); + return RecurrenceIteratorFactory.join(first, rest.toArray(new RecurrenceIterator[0])); + } + + /** + * Returns a UTC timezone object. + * @return the timezone object + */ + private static TimeZone utc() { + return TimeZone.getTimeZone("UTC"); + } + + /** + * A {@link DateIterator} with nothing in it. + */ + public static class EmptyDateIterator implements DateIterator { + public boolean hasNext() { + return false; + } + + public Date next() { + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void advanceTo(Date newStartUtc) { + //empty + } + } + + private static class ICalDateRecurrenceIterator implements RecurrenceIterator { + private final List dates; + private int index = 0; + + /* + * Note: I don't think a timezone needs to be passed in here, because it + * appears that the DateValue objects are expected to be in UTC (judging + * by the parameter name in the "advanceTo" method). + */ + public ICalDateRecurrenceIterator(List dates) { + this.dates = new ArrayList(dates); + Collections.sort(this.dates); + } + + public boolean hasNext() { + return index < dates.size(); + } + + public DateValue next() { + ICalDate next = dates.get(index++); + return convertUtc(next); + } + + public void advanceTo(DateValue newStartUtc) { + ICalDate newStart = convertUtc(newStartUtc); + while (index < dates.size() && newStart.compareTo(dates.get(index)) > 0) { + index++; + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private Google2445Utils() { + //hide + } +} diff --git a/app/src/main/java/biweekly/util/ICalDate.java b/app/src/main/java/biweekly/util/ICalDate.java new file mode 100644 index 0000000000..76fa21b7ef --- /dev/null +++ b/app/src/main/java/biweekly/util/ICalDate.java @@ -0,0 +1,141 @@ +package biweekly.util; + +import java.util.Calendar; +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a date-time value. Includes extra information that is used within + * this library. + * @author Michael Angstadt + */ +public class ICalDate extends Date { + private static final long serialVersionUID = -8172624513821588097L; + + private final DateTimeComponents rawComponents; + private final boolean hasTime; + + /** + * Creates a new date-time value set to the current date and time. + */ + public ICalDate() { + this(true); + } + + /** + * Creates a new date-time value set to the current date or time. + * @param hasTime true to include the time component, false not to + */ + public ICalDate(boolean hasTime) { + this(new Date(), null, hasTime); + } + + /** + * Creates a new date-time value (includes the time component). + * @param date the date-time value + */ + public ICalDate(Date date) { + this(date, true); + } + + /** + * Creates a new date-time value. + * @param date the date-time value + * @param hasTime true to include the time component, false not to + */ + public ICalDate(Date date, boolean hasTime) { + this(date, null, hasTime); + } + + /** + * Creates a new date-time value. + * @param date the date-time value components + * @param hasTime true to include the time component, false not to + */ + public ICalDate(DateTimeComponents date, boolean hasTime) { + this(date.toDate(), date, hasTime); + } + + /** + * Copies another iCal date-time value. + * @param date the date-time value + */ + public ICalDate(ICalDate date) { + this(date, (date.rawComponents == null) ? null : new DateTimeComponents(date.rawComponents), date.hasTime); + } + + /** + * Creates a new date-time value. + * @param date the date-time value + * @param rawComponents the raw date-time value as parsed from the input + * stream + * @param hasTime true if the date-time value has a time component, false if + * not + */ + public ICalDate(Date date, DateTimeComponents rawComponents, boolean hasTime) { + if (!hasTime) { + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + date = c.getTime(); + } + + setTime(date.getTime()); + this.rawComponents = rawComponents; + this.hasTime = hasTime; + } + + /** + * Gets the raw date-time components of the value as read from the input + * stream. + * @return the raw date-time components or null if not set + */ + public DateTimeComponents getRawComponents() { + return rawComponents; + } + + /** + * Gets whether the value contains a time component. + * @return true if the value contains a time component, false if it's + * strictly a date. + */ + public boolean hasTime() { + return hasTime; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ICalDate) { + ICalDate other = (ICalDate) obj; + if (hasTime != other.hasTime) return false; + } + return super.equals(obj); + } +} diff --git a/app/src/main/java/biweekly/util/ICalDateFormat.java b/app/src/main/java/biweekly/util/ICalDateFormat.java new file mode 100644 index 0000000000..fa77c5b70f --- /dev/null +++ b/app/src/main/java/biweekly/util/ICalDateFormat.java @@ -0,0 +1,379 @@ +package biweekly.util; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Defines all of the date formats that are used in iCalendar objects, and also + * parses/formats iCalendar dates. These date formats are defined in the ISO8601 + * specification. + * @author Michael Angstadt + */ +public enum ICalDateFormat { + //@formatter:off + /** + * Example: 20120701 + */ + DATE_BASIC( + "yyyyMMdd"), + + /** + * Example: 2012-07-01 + */ + DATE_EXTENDED( + "yyyy-MM-dd"), + + /** + * Example: 20120701T142110-0500 + */ + DATE_TIME_BASIC( + "yyyyMMdd'T'HHmmssZ"), + + /** + * Example: 20120701T142110 + */ + DATE_TIME_BASIC_WITHOUT_TZ( + "yyyyMMdd'T'HHmmss"), + + /** + * Example: 2012-07-01T14:21:10-05:00 + */ + DATE_TIME_EXTENDED( + "yyyy-MM-dd'T'HH:mm:ssZ"){ + @Override + public DateFormat getDateFormat(TimeZone timezone) { + DateFormat df = new SimpleDateFormat(formatStr, Locale.ROOT){ + private static final long serialVersionUID = -297452842012115768L; + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition){ + StringBuffer sb = super.format(date, toAppendTo, fieldPosition); + + //add a colon between the hour and minute offsets + sb.insert(sb.length()-2, ':'); + + return sb; + } + }; + + if (timezone != null){ + df.setTimeZone(timezone); + } + + return df; + } + }, + + /** + * Example: 2012-07-01T14:21:10 + */ + DATE_TIME_EXTENDED_WITHOUT_TZ( + "yyyy-MM-dd'T'HH:mm:ss"), + + /** + * Example: 20120701T192110Z + */ + UTC_TIME_BASIC( + "yyyyMMdd'T'HHmmss'Z'"){ + @Override + public DateFormat getDateFormat(TimeZone timezone) { + //always use the UTC timezone + timezone = TimeZone.getTimeZone("UTC"); + return super.getDateFormat(timezone); + } + }, + + /** + * Example: 2012-07-01T19:21:10Z + */ + UTC_TIME_EXTENDED( + "yyyy-MM-dd'T'HH:mm:ss'Z'"){ + @Override + public DateFormat getDateFormat(TimeZone timezone) { + //always use the UTC timezone + timezone = TimeZone.getTimeZone("UTC"); + return super.getDateFormat(timezone); + } + }; + //@formatter:on + + /** + * The {@link SimpleDateFormat} format string used for parsing dates. + */ + protected final String formatStr; + + /** + * @param formatStr the {@link SimpleDateFormat} format string used for + * parsing dates. + */ + ICalDateFormat(String formatStr) { + this.formatStr = formatStr; + } + + /** + * Builds a {@link DateFormat} object for parsing and formating dates in + * this ISO format. + * @return the {@link DateFormat} object + */ + public DateFormat getDateFormat() { + return getDateFormat(null); + } + + /** + * Builds a {@link DateFormat} object for parsing and formating dates in + * this ISO format. + * @param timezone the timezone the date is in or null for the default + * timezone + * @return the {@link DateFormat} object + */ + public DateFormat getDateFormat(TimeZone timezone) { + DateFormat df = new SimpleDateFormat(formatStr, Locale.ROOT); + if (timezone != null) { + df.setTimeZone(timezone); + } + return df; + } + + /** + * Formats a date in this ISO format. + * @param date the date to format + * @return the date string + */ + public String format(Date date) { + return format(date, null); + } + + /** + * Formats a date in this ISO format. + * @param date the date to format + * @param timezone the timezone to format the date in or null for the + * default timezone + * @return the date string + */ + public String format(Date date, TimeZone timezone) { + DateFormat df = getDateFormat(timezone); + return df.format(date); + } + + /** + * Parses an iCalendar date. + * @param dateStr the date string to parse (e.g. "20130609T181023Z") + * @return the parsed date + * @throws IllegalArgumentException if the date string isn't in one of the + * accepted ISO8601 formats + */ + public static Date parse(String dateStr) { + return parse(dateStr, null); + } + + /** + * Parses an iCalendar date. + * @param dateStr the date string to parse (e.g. "20130609T181023Z") + * @param timezone the timezone to parse the date under or null to use the + * JVM's default timezone. If the date string contains its own UTC offset, + * then that will be used instead. + * @return the parsed date + * @throws IllegalArgumentException if the date string isn't in one of the + * accepted ISO8601 formats + */ + public static Date parse(String dateStr, TimeZone timezone) { + TimestampPattern p = new TimestampPattern(dateStr); + if (!p.matches()) { + throw parseException(dateStr); + } + + if (p.hasOffset()) { + timezone = TimeZone.getTimeZone("UTC"); + } else if (timezone == null) { + timezone = TimeZone.getDefault(); + } + + Calendar c = Calendar.getInstance(timezone); + c.clear(); + + c.set(Calendar.YEAR, p.year()); + c.set(Calendar.MONTH, p.month() - 1); + c.set(Calendar.DATE, p.date()); + + if (p.hasTime()) { + c.set(Calendar.HOUR_OF_DAY, p.hour()); + c.set(Calendar.MINUTE, p.minute()); + c.set(Calendar.SECOND, p.second()); + c.set(Calendar.MILLISECOND, p.millisecond()); + + if (p.hasOffset()) { + c.set(Calendar.ZONE_OFFSET, p.offsetMillis()); + } + } + + return c.getTime(); + } + + /** + * Wrapper for a complex regular expression that parses multiple date + * formats. + */ + private static class TimestampPattern { + //@formatter:off + private static final Pattern regex = Pattern.compile( + "^(\\d{4})-?(\\d{2})-?(\\d{2})" + + "(" + + "T(\\d{2}):?(\\d{2}):?(\\d{2})(\\.\\d+)?" + + "(" + + "Z|([-+])((\\d{2})|((\\d{2}):?(\\d{2})))" + + ")?" + + ")?$" + ); + //@formatter:on + + private final Matcher m; + private final boolean matches; + + public TimestampPattern(String str) { + m = regex.matcher(str); + matches = m.find(); + } + + public boolean matches() { + return matches; + } + + public int year() { + return parseInt(1); + } + + public int month() { + return parseInt(2); + } + + public int date() { + return parseInt(3); + } + + public boolean hasTime() { + return m.group(5) != null; + } + + public int hour() { + return parseInt(5); + } + + public int minute() { + return parseInt(6); + } + + public int second() { + return parseInt(7); + } + + public int millisecond() { + if (m.group(8) == null) { + return 0; + } + + double ms = Double.parseDouble(m.group(8)) * 1000; + return (int) Math.round(ms); + } + + public boolean hasOffset() { + return m.group(9) != null; + } + + public int offsetMillis() { + if (m.group(9).equals("Z")) { + return 0; + } + + int positive = m.group(10).equals("+") ? 1 : -1; + + int offsetHour, offsetMinute; + if (m.group(12) != null) { + offsetHour = parseInt(12); + offsetMinute = 0; + } else { + offsetHour = parseInt(14); + offsetMinute = parseInt(15); + } + + return (offsetHour * 60 * 60 * 1000 + offsetMinute * 60 * 1000) * positive; + } + + private int parseInt(int group) { + return Integer.parseInt(m.group(group)); + } + } + + /** + * Determines whether a date string has a time component. + * @param dateStr the date string (e.g. "20130601T120000") + * @return true if it has a time component, false if not + */ + public static boolean dateHasTime(String dateStr) { + return dateStr.contains("T"); + } + + /** + * Determines whether a date string is in UTC time or has a timezone offset. + * @param dateStr the date string (e.g. "20130601T120000Z", + * "20130601T120000-0400") + * @return true if it has a timezone, false if not + */ + public static boolean dateHasTimezone(String dateStr) { + return isUTC(dateStr) || dateStr.matches(".*?[-+]\\d\\d:?\\d\\d"); + } + + /** + * Determines if a date string is in UTC time. + * @param dateStr the date string (e.g. "20130601T120000Z") + * @return true if it's in UTC, false if not + */ + public static boolean isUTC(String dateStr) { + return dateStr.endsWith("Z"); + } + + /** + * Gets the {@link TimeZone} object that corresponds to the given ID. + * @param timezoneId the timezone ID (e.g. "America/New_York") + * @return the timezone object or null if not found + */ + public static TimeZone parseTimeZoneId(String timezoneId) { + TimeZone timezone = TimeZone.getTimeZone(timezoneId); + return "GMT".equals(timezone.getID()) && !"GMT".equalsIgnoreCase(timezoneId) ? null : timezone; + } + + private static IllegalArgumentException parseException(String dateStr) { + return new IllegalArgumentException("Date string \"" + dateStr + "\" is not in a valid ISO-8601 format."); + } +} diff --git a/app/src/main/java/biweekly/util/ICalFloatFormatter.java b/app/src/main/java/biweekly/util/ICalFloatFormatter.java new file mode 100644 index 0000000000..de04807fce --- /dev/null +++ b/app/src/main/java/biweekly/util/ICalFloatFormatter.java @@ -0,0 +1,71 @@ +package biweekly.util; + +import java.text.NumberFormat; +import java.util.Locale; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Formats floating-point values for iCalendar objects. This ensures that numbers + * are rendered the same, no matter the default locale. + *

+ *
    + *
  • Decimal separator can differ by locale (e.g. Germany uses ",")
  • + *
  • Number characters can differ by locale (e.g. "1.0" is "Û±Ù«Û°" in Iran)
  • + *
+ * @author Michael Angstadt + */ +public class ICalFloatFormatter { + private final NumberFormat nf = NumberFormat.getNumberInstance(Locale.ROOT); + + /** + * Creates a new formatter with a max of 6 decimals. + */ + public ICalFloatFormatter() { + this(6); + } + + /** + * Creates a new formatter. + * @param decimals the max number of decimal places + */ + public ICalFloatFormatter(int decimals) { + nf.setMaximumFractionDigits(decimals); + if (decimals > 0) { + nf.setMinimumFractionDigits(1); + } + } + + /** + * Formats a number for inclusion in an iCalendar object. + * @param number the number + * @return the formatted number + */ + public String format(double number) { + return nf.format(number); + } +} diff --git a/app/src/main/java/biweekly/util/IOUtils.java b/app/src/main/java/biweekly/util/IOUtils.java new file mode 100644 index 0000000000..f9d8cc3d2f --- /dev/null +++ b/app/src/main/java/biweekly/util/IOUtils.java @@ -0,0 +1,53 @@ +package biweekly.util; + +import java.io.Closeable; +import java.io.IOException; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * I/O helper classes. + * @author Michael Angstadt + */ +public final class IOUtils { + /** + * Closes a closeable resource, catching its {@link IOException}. + * @param closeable the resource to close (can be null) + */ + public static void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException e) { + //ignore + } + } + + private IOUtils() { + //hide + } +} diff --git a/app/src/main/java/biweekly/util/ListMultimap.java b/app/src/main/java/biweekly/util/ListMultimap.java new file mode 100644 index 0000000000..f5c20e5c83 --- /dev/null +++ b/app/src/main/java/biweekly/util/ListMultimap.java @@ -0,0 +1,746 @@ +package biweekly.util; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A multimap that uses {@link ArrayList} objects to store its values. The + * internal {@link Map} implementation is a {@link LinkedHashMap}. + * @author Michael Angstadt + * @param the key + * @param the value + */ +public class ListMultimap implements Iterable>> { + private final Map> map; + + /** + * Creates an empty multimap. + */ + public ListMultimap() { + this(new LinkedHashMap>()); + } + + /** + * Creates an empty multimap. + * @param initialCapacity the initial capacity of the underlying map. + */ + public ListMultimap(int initialCapacity) { + this(new LinkedHashMap>(initialCapacity)); + } + + /** + * Creates a copy of an existing multimap. + * @param orig the multimap to copy from + */ + public ListMultimap(ListMultimap orig) { + this(copy(orig.map)); + } + + private static Map> copy(Map> orig) { + Map> map = new LinkedHashMap>(orig.size()); + for (Map.Entry> entry : orig.entrySet()) { + List values = new ArrayList(entry.getValue()); + map.put(entry.getKey(), values); + } + return map; + } + + /** + *

+ * Creates a new multimap backed by the given map. Changes made to the given + * map will effect the multimap and vice versa. + *

+ *

+ * To avoid problems, it is highly recommended that the given map NOT be + * modified by anything other than this {@link ListMultimap} class after + * being passed into this constructor. + *

+ * @param map the backing map + */ + public ListMultimap(Map> map) { + this.map = map; + } + + /** + * Adds a value to the multimap. + * @param key the key + * @param value the value to add + */ + public void put(K key, V value) { + key = sanitizeKey(key); + List list = map.get(key); + if (list == null) { + list = new ArrayList(); + map.put(key, list); + } + list.add(value); + } + + /** + * Adds multiple values to the multimap. + * @param key the key + * @param values the values to add + */ + public void putAll(K key, Collection values) { + if (values.isEmpty()) { + return; + } + + key = sanitizeKey(key); + List list = map.get(key); + if (list == null) { + list = new ArrayList(); + map.put(key, list); + } + list.addAll(values); + } + + /** + * Gets the values associated with the key. Changes to the returned list + * will update the underlying multimap, and vice versa. + * @param key the key + * @return the list of values or empty list if the key doesn't exist + */ + public List get(K key) { + key = sanitizeKey(key); + List value = map.get(key); + if (value == null) { + value = new ArrayList(0); + } + return new WrappedList(key, value, null); + } + + /** + * Gets the first value that's associated with a key. + * @param key the key + * @return the first value or null if the key doesn't exist + */ + public V first(K key) { + key = sanitizeKey(key); + List values = map.get(key); + + /* + * The list can be null, but never empty. Empty lists are removed from + * the map. + */ + return (values == null) ? null : values.get(0); + } + + /** + * Determines whether the given key exists. + * @param key the key + * @return true if the key exists, false if not + */ + public boolean containsKey(K key) { + key = sanitizeKey(key); + return map.containsKey(key); + } + + /** + * Removes a particular value. + * @param key the key + * @param value the value to remove + * @return true if the multimap contained the value, false if not + */ + public boolean remove(K key, V value) { + key = sanitizeKey(key); + List values = map.get(key); + if (values == null) { + return false; + } + + boolean success = values.remove(value); + if (values.isEmpty()) { + map.remove(key); + } + return success; + } + + /** + * Removes all the values associated with a key + * @param key the key to remove + * @return the removed values or an empty list if the key doesn't exist + * (this list is immutable) + */ + public List removeAll(K key) { + key = sanitizeKey(key); + List removed = map.remove(key); + if (removed == null) { + return Collections.emptyList(); + } + + List unmodifiableCopy = Collections.unmodifiableList(new ArrayList(removed)); + removed.clear(); + return unmodifiableCopy; + } + + /** + * Replaces all values with the given value. + * @param key the key + * @param value the value with which to replace all existing values, or null + * to remove all values + * @return the values that were replaced (this list is immutable) + */ + public List replace(K key, V value) { + List replaced = removeAll(key); + if (value != null) { + put(key, value); + } + return replaced; + } + + /** + * Replaces all values with the given values. + * @param key the key + * @param values the values with which to replace all existing values + * @return the values that were replaced (this list is immutable) + */ + public List replace(K key, Collection values) { + List replaced = removeAll(key); + putAll(key, values); + return replaced; + } + + /** + * Clears all entries from the multimap. + */ + public void clear() { + //clear each collection to make previously returned lists empty + for (List value : map.values()) { + value.clear(); + } + map.clear(); + } + + /** + * Gets all the keys in the multimap. + * @return the keys (this set is immutable) + */ + public Set keySet() { + return Collections.unmodifiableSet(map.keySet()); + } + + /** + * Gets all the values in the multimap. + * @return the values (this list is immutable) + */ + public List values() { + List list = new ArrayList(); + for (List value : map.values()) { + list.addAll(value); + } + return Collections.unmodifiableList(list); + } + + /** + * Determines if the multimap is empty or not. + * @return true if it's empty, false if not + */ + public boolean isEmpty() { + return size() == 0; + } + + /** + * Gets the number of values in the map. + * @return the number of values + */ + public int size() { + int size = 0; + for (List value : map.values()) { + size += value.size(); + } + return size; + } + + /** + * Gets an immutable view of the underlying {@link Map} object. + * @return an immutable map + */ + public Map> asMap() { + Map> view = new LinkedHashMap>(map.size()); + for (Map.Entry> entry : map.entrySet()) { + K key = entry.getKey(); + List value = entry.getValue(); + view.put(key, Collections.unmodifiableList(value)); + } + return Collections.unmodifiableMap(view); + } + + /** + * Gets the {@link Map} that backs this multimap. This method is here for + * performances reasons. The returned map should NOT be modified by anything + * other than the {@link ListMultimap} object that owns it. + * @return the map + */ + public Map> getMap() { + return map; + } + + /** + * Modifies a given key before it is used to interact with the internal map. + * This method is meant to be overridden by child classes if necessary. + * @param key the key + * @return the modified key (by default, the key is returned as-is) + */ + protected K sanitizeKey(K key) { + return key; + } + + /** + * Gets an iterator for iterating over the entries in the map. This iterator + * iterates over an immutable view of the map. + * @return the iterator + */ + //@Override + public Iterator>> iterator() { + final Iterator>> it = map.entrySet().iterator(); + return new Iterator>>() { + public boolean hasNext() { + return it.hasNext(); + } + + public Entry> next() { + final Entry> next = it.next(); + return new Entry>() { + public K getKey() { + return next.getKey(); + } + + public List getValue() { + return Collections.unmodifiableList(next.getValue()); + } + + public List setValue(List value) { + throw new UnsupportedOperationException(); + } + }; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public String toString() { + return map.toString(); + } + + @Override + public int hashCode() { + return map.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + ListMultimap other = (ListMultimap) obj; + return map.equals(other.map); + } + + /** + * Note: This class is a modified version of the + * "AbstractMapBasedMultimap.WrappedList" class from the + * Guava. + * + *

+ * Collection decorator that stays in sync with the multimap values for a + * key. There are two kinds of wrapped collections: full and subcollections. + * Both have a delegate pointing to the underlying collection class. + * + *

+ * Full collections, identified by a null ancestor field, contain all + * multimap values for a given key. Its delegate is a value in the + * multimap's underlying {@link Map} whenever the delegate is non-empty. The + * {@code refreshIfEmpty}, {@code removeIfEmpty}, and {@code addToMap} + * methods ensure that the {@code WrappedList} and map remain consistent. + * + *

+ * A subcollection, such as a sublist, contains some of the values for a + * given key. Its ancestor field points to the full wrapped collection with + * all values for the key. The subcollection {@code refreshIfEmpty}, + * {@code removeIfEmpty}, and {@code addToMap} methods call the + * corresponding methods of the full wrapped collection. + */ + private class WrappedList extends AbstractCollection implements List { + final K key; + List delegate; + final WrappedList ancestor; + final List ancestorDelegate; + + WrappedList(K key, List delegate, WrappedList ancestor) { + this.key = key; + this.delegate = delegate; + this.ancestor = ancestor; + this.ancestorDelegate = (ancestor == null) ? null : ancestor.getDelegate(); + } + + public boolean addAll(int index, Collection collection) { + if (collection.isEmpty()) { + return false; + } + int oldSize = size(); // calls refreshIfEmpty + boolean changed = getDelegate().addAll(index, collection); + if (changed && oldSize == 0) { + addToMap(); + } + return changed; + } + + public V get(int index) { + refreshIfEmpty(); + return getDelegate().get(index); + } + + public V set(int index, V element) { + refreshIfEmpty(); + return getDelegate().set(index, element); + } + + public void add(int index, V element) { + refreshIfEmpty(); + boolean wasEmpty = getDelegate().isEmpty(); + getDelegate().add(index, element); + if (wasEmpty) { + addToMap(); + } + } + + public V remove(int index) { + refreshIfEmpty(); + V value = getDelegate().remove(index); + removeIfEmpty(); + return value; + } + + public int indexOf(Object o) { + refreshIfEmpty(); + return getDelegate().indexOf(o); + } + + public int lastIndexOf(Object o) { + refreshIfEmpty(); + return getDelegate().lastIndexOf(o); + } + + public ListIterator listIterator() { + refreshIfEmpty(); + return new WrappedListIterator(); + } + + public ListIterator listIterator(int index) { + refreshIfEmpty(); + return new WrappedListIterator(index); + } + + public List subList(int fromIndex, int toIndex) { + refreshIfEmpty(); + return new WrappedList(getKey(), getDelegate().subList(fromIndex, toIndex), (getAncestor() == null) ? this : getAncestor()); + } + + /** + * If the delegate collection is empty, but the multimap has values for + * the key, replace the delegate with the new collection for the key. + * + *

+ * For a subcollection, refresh its ancestor and validate that the + * ancestor delegate hasn't changed. + */ + void refreshIfEmpty() { + if (ancestor != null) { + ancestor.refreshIfEmpty(); + if (ancestor.getDelegate() != ancestorDelegate) { + throw new ConcurrentModificationException(); + } + } else if (delegate.isEmpty()) { + List newDelegate = map.get(key); + if (newDelegate != null) { + delegate = newDelegate; + } + } + } + + /** + * If collection is empty, remove it from + * {@code AbstractMapBasedMultimap.this.map}. For subcollections, check + * whether the ancestor collection is empty. + */ + void removeIfEmpty() { + if (ancestor != null) { + ancestor.removeIfEmpty(); + } else if (delegate.isEmpty()) { + map.remove(key); + } + } + + K getKey() { + return key; + } + + /** + * Add the delegate to the map. Other {@code WrappedCollection} methods + * should call this method after adding elements to a previously empty + * collection. + * + *

+ * Subcollection add the ancestor's delegate instead. + */ + void addToMap() { + if (ancestor != null) { + ancestor.addToMap(); + } else { + map.put(key, delegate); + } + } + + @Override + public int size() { + refreshIfEmpty(); + return delegate.size(); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + refreshIfEmpty(); + return delegate.equals(object); + } + + @Override + public int hashCode() { + refreshIfEmpty(); + return delegate.hashCode(); + } + + @Override + public String toString() { + refreshIfEmpty(); + return delegate.toString(); + } + + List getDelegate() { + return delegate; + } + + @Override + public Iterator iterator() { + refreshIfEmpty(); + return new WrappedListIterator(); + } + + @Override + public boolean add(V value) { + refreshIfEmpty(); + boolean wasEmpty = delegate.isEmpty(); + boolean changed = delegate.add(value); + if (changed && wasEmpty) { + addToMap(); + } + return changed; + } + + WrappedList getAncestor() { + return ancestor; + } + + // The following methods are provided for better performance. + + @Override + public boolean addAll(Collection collection) { + if (collection.isEmpty()) { + return false; + } + int oldSize = size(); // calls refreshIfEmpty + boolean changed = delegate.addAll(collection); + if (changed && oldSize == 0) { + addToMap(); + } + return changed; + } + + @Override + public boolean contains(Object o) { + refreshIfEmpty(); + return delegate.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + refreshIfEmpty(); + return delegate.containsAll(c); + } + + @Override + public void clear() { + int oldSize = size(); // calls refreshIfEmpty + if (oldSize == 0) { + return; + } + delegate.clear(); + removeIfEmpty(); // maybe shouldn't be removed if this is a sublist + } + + @Override + public boolean remove(Object o) { + refreshIfEmpty(); + boolean changed = delegate.remove(o); + if (changed) { + removeIfEmpty(); + } + return changed; + } + + @Override + public boolean removeAll(Collection collection) { + if (collection.isEmpty()) { + return false; + } + refreshIfEmpty(); + boolean changed = delegate.removeAll(collection); + if (changed) { + removeIfEmpty(); + } + return changed; + } + + @Override + public boolean retainAll(Collection c) { + refreshIfEmpty(); + boolean changed = delegate.retainAll(c); + if (changed) { + removeIfEmpty(); + } + return changed; + } + + /** ListIterator decorator. */ + private class WrappedListIterator implements ListIterator { + final ListIterator delegateIterator; + final List originalDelegate = delegate; + + WrappedListIterator() { + delegateIterator = delegate.listIterator(); + } + + public WrappedListIterator(int index) { + delegateIterator = delegate.listIterator(index); + } + + public boolean hasPrevious() { + return getDelegateIterator().hasPrevious(); + } + + public V previous() { + return getDelegateIterator().previous(); + } + + public int nextIndex() { + return getDelegateIterator().nextIndex(); + } + + public int previousIndex() { + return getDelegateIterator().previousIndex(); + } + + public void set(V value) { + getDelegateIterator().set(value); + } + + public void add(V value) { + boolean wasEmpty = isEmpty(); + getDelegateIterator().add(value); + if (wasEmpty) { + addToMap(); + } + } + + /** + * If the delegate changed since the iterator was created, the + * iterator is no longer valid. + */ + void validateIterator() { + refreshIfEmpty(); + if (delegate != originalDelegate) { + throw new ConcurrentModificationException(); + } + } + + public boolean hasNext() { + validateIterator(); + return delegateIterator.hasNext(); + } + + public V next() { + validateIterator(); + return delegateIterator.next(); + } + + public void remove() { + delegateIterator.remove(); + removeIfEmpty(); + } + + ListIterator getDelegateIterator() { + validateIterator(); + return delegateIterator; + } + } + } +} diff --git a/app/src/main/java/biweekly/util/Period.java b/app/src/main/java/biweekly/util/Period.java new file mode 100644 index 0000000000..b86ae46c91 --- /dev/null +++ b/app/src/main/java/biweekly/util/Period.java @@ -0,0 +1,160 @@ +package biweekly.util; + +import java.util.Date; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * A period of time. + * @author Michael Angstadt + */ +public final class Period { + /* + * Note: The getter methods must not make copies of the date objects they + * return! The date objects in this class must be directly referenced in + * order to support timezones. + * + * Yes, this means that this class is not fully immutable. Date objects can + * be modified by calling "setTime()", but biweekly makes use of this method + * in order to convert dates into their proper timezones. + */ + private final ICalDate startDate; + private final ICalDate endDate; + private final Duration duration; + + /** + * Creates a new time period. + * @param startDate the start date + * @param endDate the end date + */ + public Period(Date startDate, Date endDate) { + //@formatter:off + this( + (startDate == null) ? null : new ICalDate(startDate), + (endDate == null) ? null : new ICalDate(endDate) + ); + //@formatter:on + } + + /** + * Creates a new time period. + * @param startDate the start date + * @param endDate the end date + */ + public Period(ICalDate startDate, ICalDate endDate) { + this.startDate = startDate; + this.endDate = endDate; + duration = null; + } + + /** + * Creates a new time period. + * @param startDate the start date + * @param duration the length of time after the start date + */ + public Period(Date startDate, Duration duration) { + //@formatter:off + this( + (startDate == null) ? null : new ICalDate(startDate), + duration + ); + //@formatter:on + } + + /** + * Creates a new time period. + * @param startDate the start date + * @param duration the length of time after the start date + */ + public Period(ICalDate startDate, Duration duration) { + this.startDate = startDate; + this.duration = duration; + endDate = null; + } + + /** + * Copies an existing time period. + * @param period the period to copy + */ + public Period(Period period) { + this.startDate = (period.startDate == null) ? null : new ICalDate(period.startDate); + this.endDate = (period.endDate == null) ? null : new ICalDate(period.endDate); + this.duration = period.duration; + } + + /** + * Gets the start date. + * @return the start date + */ + public ICalDate getStartDate() { + return startDate; + } + + /** + * Gets the end date. This will be null if a duration was defined. + * @return the end date or null if not set + */ + public ICalDate getEndDate() { + return endDate; + } + + /** + * Gets the length of time after the start date. This will be null if an end + * date was defined. + * @return the duration or null if not set + */ + public Duration getDuration() { + return duration; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((duration == null) ? 0 : duration.hashCode()); + result = prime * result + ((endDate == null) ? 0 : endDate.hashCode()); + result = prime * result + ((startDate == null) ? 0 : startDate.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + Period other = (Period) obj; + if (duration == null) { + if (other.duration != null) return false; + } else if (!duration.equals(other.duration)) return false; + if (endDate == null) { + if (other.endDate != null) return false; + } else if (!endDate.equals(other.endDate)) return false; + if (startDate == null) { + if (other.startDate != null) return false; + } else if (!startDate.equals(other.startDate)) return false; + return true; + } +} diff --git a/app/src/main/java/biweekly/util/Recurrence.java b/app/src/main/java/biweekly/util/Recurrence.java new file mode 100644 index 0000000000..555c7dd01e --- /dev/null +++ b/app/src/main/java/biweekly/util/Recurrence.java @@ -0,0 +1,651 @@ +package biweekly.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import biweekly.property.DateStart; +import biweekly.util.com.google.ical.compat.javautil.DateIterator; +import biweekly.util.com.google.ical.compat.javautil.DateIteratorFactory; +import biweekly.util.com.google.ical.iter.RecurrenceIterator; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + *

+ * Represents a recurrence rule value. + *

+ *

+ * This class is immutable. Use the inner class {@link Builder} to construct a + * new instance. + *

+ *

+ * Code sample: + *

+ * + *
+ * Recurrence rrule = new Recurrence.Builder(Frequency.WEEKLY).interval(2).build();
+ * Recurrence copy = new Recurrence.Builder(rrule).interval(3).build();
+ * 
+ * @author Michael Angstadt + * @see RFC 5545 + * p.38-45 + */ +public final class Recurrence { + private final Frequency frequency; + private final Integer interval; + private final Integer count; + private final ICalDate until; + private final List bySecond; + private final List byMinute; + private final List byHour; + private final List byMonthDay; + private final List byYearDay; + private final List byWeekNo; + private final List byMonth; + private final List bySetPos; + private final List byDay; + private final DayOfWeek workweekStarts; + private final Map> xrules; + + private Recurrence(Builder builder) { + frequency = builder.frequency; + interval = builder.interval; + count = builder.count; + until = builder.until; + bySecond = Collections.unmodifiableList(builder.bySecond); + byMinute = Collections.unmodifiableList(builder.byMinute); + byHour = Collections.unmodifiableList(builder.byHour); + byMonthDay = Collections.unmodifiableList(builder.byMonthDay); + byYearDay = Collections.unmodifiableList(builder.byYearDay); + byWeekNo = Collections.unmodifiableList(builder.byWeekNo); + byMonth = Collections.unmodifiableList(builder.byMonth); + bySetPos = Collections.unmodifiableList(builder.bySetPos); + byDay = Collections.unmodifiableList(builder.byDay); + workweekStarts = builder.workweekStarts; + xrules = Collections.unmodifiableMap(builder.xrules.getMap()); + } + + /** + * Gets the frequency. + * @return the frequency or null if not set + */ + public Frequency getFrequency() { + return frequency; + } + + /** + * Gets the date that the recurrence stops. + * @return the date or null if not set + */ + public ICalDate getUntil() { + return (until == null) ? null : new ICalDate(until); + } + + /** + * Gets the number of times the rule will be repeated. + * @return the number of times to repeat the rule or null if not set + */ + public Integer getCount() { + return count; + } + + /** + * Gets how often the rule repeats, in relation to the frequency. + * @return the repetition interval or null if not set + */ + public Integer getInterval() { + return interval; + } + + /** + * Gets the BYSECOND rule part. + * @return the BYSECOND rule part or empty list if not set + */ + public List getBySecond() { + return bySecond; + } + + /** + * Gets the BYMINUTE rule part. + * @return the BYMINUTE rule part or empty list if not set + */ + public List getByMinute() { + return byMinute; + } + + /** + * Gets the BYHOUR rule part. + * @return the BYHOUR rule part or empty list if not set + */ + public List getByHour() { + return byHour; + } + + /** + * Gets the day components of the BYDAY rule part. + * @return the day components of the BYDAY rule part or empty list if not + * set + */ + public List getByDay() { + return byDay; + } + + /** + * Gets the BYMONTHDAY rule part. + * @return the BYMONTHDAY rule part or empty list if not set + */ + public List getByMonthDay() { + return byMonthDay; + } + + /** + * Gets the BYYEARDAY rule part. + * @return the BYYEARDAY rule part or empty list if not set + */ + public List getByYearDay() { + return byYearDay; + } + + /** + * Gets the BYWEEKNO rule part. + * @return the BYWEEKNO rule part or empty list if not set + */ + public List getByWeekNo() { + return byWeekNo; + } + + /** + * Gets the BYMONTH rule part. + * @return the BYMONTH rule part or empty list if not set + */ + public List getByMonth() { + return byMonth; + } + + /** + * Gets the BYSETPOS rule part. + * @return the BYSETPOS rule part or empty list if not set + */ + public List getBySetPos() { + return bySetPos; + } + + /** + * Gets the day that the work week starts. + * @return the day that the work week starts or null if not set + */ + public DayOfWeek getWorkweekStarts() { + return workweekStarts; + } + + /** + * Gets the non-standard rule parts. + * @return the non-standard rule parts + */ + public Map> getXRules() { + return xrules; + } + + /** + * Creates an iterator that computes the dates defined by this recurrence. + * @param startDate the date that the recurrence starts (typically, the + * value of the {@link DateStart} property) + * @param timezone the timezone to iterate in (typically, the timezone + * associated with the {@link DateStart} property). This is needed in order + * to adjust for when the iterator passes over a daylight savings boundary. + * @return the iterator + * @see google-rfc-2445 + */ + public DateIterator getDateIterator(Date startDate, TimeZone timezone) { + return getDateIterator(new ICalDate(startDate), timezone); + } + + /** + * Creates an iterator that computes the dates defined by this recurrence. + * @param startDate the date that the recurrence starts (typically, the + * value of the {@link DateStart} property) + * @param timezone the timezone to iterate in (typically, the timezone + * associated with the {@link DateStart} property). This is needed in order + * to adjust for when the iterator passes over a daylight savings boundary. + * @return the iterator + * @see google-rfc-2445 + */ + public DateIterator getDateIterator(ICalDate startDate, TimeZone timezone) { + RecurrenceIterator iterator = Google2445Utils.createRecurrenceIterator(this, startDate, timezone); + return DateIteratorFactory.createDateIterator(iterator); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + byDay.hashCode(); + result = prime * result + byHour.hashCode(); + result = prime * result + byMinute.hashCode(); + result = prime * result + byMonth.hashCode(); + result = prime * result + byMonthDay.hashCode(); + result = prime * result + bySecond.hashCode(); + result = prime * result + bySetPos.hashCode(); + result = prime * result + byWeekNo.hashCode(); + result = prime * result + byYearDay.hashCode(); + result = prime * result + ((count == null) ? 0 : count.hashCode()); + result = prime * result + xrules.hashCode(); + result = prime * result + ((frequency == null) ? 0 : frequency.hashCode()); + result = prime * result + ((interval == null) ? 0 : interval.hashCode()); + result = prime * result + ((until == null) ? 0 : until.hashCode()); + result = prime * result + ((workweekStarts == null) ? 0 : workweekStarts.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + Recurrence other = (Recurrence) obj; + if (!byDay.equals(other.byDay)) return false; + if (!byHour.equals(other.byHour)) return false; + if (!byMinute.equals(other.byMinute)) return false; + if (!byMonth.equals(other.byMonth)) return false; + if (!byMonthDay.equals(other.byMonthDay)) return false; + if (!bySecond.equals(other.bySecond)) return false; + if (!bySetPos.equals(other.bySetPos)) return false; + if (!byWeekNo.equals(other.byWeekNo)) return false; + if (!byYearDay.equals(other.byYearDay)) return false; + if (count == null) { + if (other.count != null) return false; + } else if (!count.equals(other.count)) return false; + if (!xrules.equals(other.xrules)) return false; + if (frequency != other.frequency) return false; + if (interval == null) { + if (other.interval != null) return false; + } else if (!interval.equals(other.interval)) return false; + if (until == null) { + if (other.until != null) return false; + } else if (!until.equals(other.until)) return false; + if (workweekStarts != other.workweekStarts) return false; + return true; + } + + /** + * Constructs {@link Recurrence} objects. + * @author Michael Angstadt + */ + public static class Builder { + private Frequency frequency; + private Integer interval; + private Integer count; + private ICalDate until; + private List bySecond; + private List byMinute; + private List byHour; + private List byDay; + private List byMonthDay; + private List byYearDay; + private List byWeekNo; + private List byMonth; + private List bySetPos; + private DayOfWeek workweekStarts; + private ListMultimap xrules; + + /** + * Constructs a new builder. + * @param frequency the recurrence frequency + */ + public Builder(Frequency frequency) { + this.frequency = frequency; + bySecond = new ArrayList(0); + byMinute = new ArrayList(0); + byHour = new ArrayList(0); + byDay = new ArrayList(0); + byMonthDay = new ArrayList(0); + byYearDay = new ArrayList(0); + byWeekNo = new ArrayList(0); + byMonth = new ArrayList(0); + bySetPos = new ArrayList(0); + xrules = new ListMultimap(0); + } + + /** + * Constructs a new builder + * @param recur the recurrence object to copy from + */ + public Builder(Recurrence recur) { + frequency = recur.frequency; + interval = recur.interval; + count = recur.count; + until = recur.until; + bySecond = new ArrayList(recur.bySecond); + byMinute = new ArrayList(recur.byMinute); + byHour = new ArrayList(recur.byHour); + byDay = new ArrayList(recur.byDay); + byMonthDay = new ArrayList(recur.byMonthDay); + byYearDay = new ArrayList(recur.byYearDay); + byWeekNo = new ArrayList(recur.byWeekNo); + byMonth = new ArrayList(recur.byMonth); + bySetPos = new ArrayList(recur.bySetPos); + workweekStarts = recur.workweekStarts; + + Map> map = new HashMap>(recur.xrules); + xrules = new ListMultimap(map); + } + + /** + * Sets the frequency + * @param frequency the frequency + * @return this + */ + public Builder frequency(Frequency frequency) { + this.frequency = frequency; + return this; + } + + /** + * Sets the date that the recurrence stops. Note that the UNTIL and + * COUNT fields cannot both be defined within the same rule. + * @param until the date + * @return this + */ + public Builder until(ICalDate until) { + this.until = (until == null) ? null : new ICalDate(until); + return this; + } + + /** + * Sets the date that the recurrence stops. Note that the UNTIL and + * COUNT fields cannot both be defined within the same rule. + * @param until the date (time component will be included) + * @return this + */ + public Builder until(Date until) { + return until(until, true); + } + + /** + * Sets the date that the recurrence stops. Note that the UNTIL and + * COUNT fields cannot both be defined within the same rule. + * @param until the date + * @param hasTime true to include the time component, false if it's + * strictly a date + * @return this + */ + public Builder until(Date until, boolean hasTime) { + this.until = new ICalDate(until, hasTime); + return this; + } + + /** + * Gets the number of times the rule will be repeated. Note that the + * UNTIL and COUNT fields cannot both be defined within the same rule. + * @param count the number of times to repeat the rule + * @return this + */ + public Builder count(Integer count) { + this.count = count; + return this; + } + + /** + * Gets how often the rule repeats, in relation to the frequency. + * @param interval the repetition interval + * @return this + */ + public Builder interval(Integer interval) { + this.interval = interval; + return this; + } + + /** + * Adds one or more BYSECOND rule parts. + * @param seconds the seconds to add + * @return this + */ + public Builder bySecond(Integer... seconds) { + return bySecond(Arrays.asList(seconds)); + } + + /** + * Adds one or more BYSECOND rule parts. + * @param seconds the seconds to add + * @return this + */ + public Builder bySecond(Collection seconds) { + bySecond.addAll(seconds); + return this; + } + + /** + * Adds one or more BYMINUTE rule parts. + * @param minutes the minutes to add + * @return this + */ + public Builder byMinute(Integer... minutes) { + return byMinute(Arrays.asList(minutes)); + } + + /** + * Adds one or more BYMINUTE rule parts. + * @param minutes the minutes to add + * @return this + */ + public Builder byMinute(Collection minutes) { + byMinute.addAll(minutes); + return this; + } + + /** + * Adds one or more BYHOUR rule parts. + * @param hours the hours to add + * @return this + */ + public Builder byHour(Integer... hours) { + return byHour(Arrays.asList(hours)); + } + + /** + * Adds one or more BYHOUR rule parts. + * @param hours the hours to add + * @return this + */ + public Builder byHour(Collection hours) { + this.byHour.addAll(hours); + return this; + } + + /** + * Adds one or more BYMONTHDAY rule parts. + * @param monthDays the month days to add + * @return this + */ + public Builder byMonthDay(Integer... monthDays) { + return byMonthDay(Arrays.asList(monthDays)); + } + + /** + * Adds one or more BYMONTHDAY rule parts. + * @param monthDays the month days to add + * @return this + */ + public Builder byMonthDay(Collection monthDays) { + byMonthDay.addAll(monthDays); + return this; + } + + /** + * Adds one or more BYYEARDAY rule parts. + * @param yearDays the year days to add + * @return this + */ + public Builder byYearDay(Integer... yearDays) { + return byYearDay(Arrays.asList(yearDays)); + } + + /** + * Adds one or more BYYEARDAY rule parts. + * @param yearDays the year days to add + * @return this + */ + public Builder byYearDay(Collection yearDays) { + byYearDay.addAll(yearDays); + return this; + } + + /** + * Adds one or more BYWEEKNO rule parts. + * @param weekNumbers the week numbers to add + * @return this + */ + public Builder byWeekNo(Integer... weekNumbers) { + return byWeekNo(Arrays.asList(weekNumbers)); + } + + /** + * Adds one or more BYWEEKNO rule parts. + * @param weekNumbers the week numbers to add + * @return this + */ + public Builder byWeekNo(Collection weekNumbers) { + byWeekNo.addAll(weekNumbers); + return this; + } + + /** + * Adds one or more BYMONTH rule parts. + * @param months the months to add + * @return this + */ + public Builder byMonth(Integer... months) { + return byMonth(Arrays.asList(months)); + } + + /** + * Adds one or more BYMONTH rule parts. + * @param months the months to add + * @return this + */ + public Builder byMonth(Collection months) { + byMonth.addAll(months); + return this; + } + + /** + * Adds one or more BYSETPOS rule parts. + * @param positions the values to add + * @return this + */ + public Builder bySetPos(Integer... positions) { + return bySetPos(Arrays.asList(positions)); + } + + /** + * Adds one or more BYSETPOS rule parts. + * @param positions the values to add + * @return this + */ + public Builder bySetPos(Collection positions) { + bySetPos.addAll(positions); + return this; + } + + /** + * Adds one or more BYDAY rule parts. + * @param days the days to add + * @return this + */ + public Builder byDay(DayOfWeek... days) { + return byDay(Arrays.asList(days)); + } + + /** + * Adds one or more BYDAY rule parts. + * @param days the days to add + * @return this + */ + public Builder byDay(Collection days) { + for (DayOfWeek day : days) { + byDay(null, day); + } + return this; + } + + /** + * Adds a BYDAY rule part. + * @param num the numeric component + * @param day the day to add + * @return this + */ + public Builder byDay(Integer num, DayOfWeek day) { + byDay.add(new ByDay(num, day)); + return this; + } + + /** + * Sets the day that the work week starts. + * @param day the day + * @return this + */ + public Builder workweekStarts(DayOfWeek day) { + workweekStarts = day; + return this; + } + + /** + * Adds a non-standard rule part. + * @param name the name + * @param value the value or null to remove the rule part + * @return this + */ + public Builder xrule(String name, String value) { + name = name.toUpperCase(); + + if (value == null) { + xrules.removeAll(name); + } else { + xrules.put(name, value); + } + + return this; + } + + /** + * Builds the final {@link Recurrence} object. + * @return the object + */ + public Recurrence build() { + return new Recurrence(this); + } + } +} diff --git a/app/src/main/java/biweekly/util/StringUtils.java b/app/src/main/java/biweekly/util/StringUtils.java new file mode 100644 index 0000000000..194af331b0 --- /dev/null +++ b/app/src/main/java/biweekly/util/StringUtils.java @@ -0,0 +1,163 @@ +package biweekly.util; + +import java.util.Collection; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Helper class for dealing with strings. + * @author Michael Angstadt + */ +public final class StringUtils { + /** + * The local computer's newline character sequence. + */ + public static final String NEWLINE = System.getProperty("line.separator"); + + /** + *

+ * Returns a substring of the given string that comes after the given + * prefix. Prefix matching is case-insensitive. + *

+ *

+ * Example: + *

+ * + *
+	 * String result = StringUtils.afterPrefixIgnoreCase("MAILTO:email@example.com", "mailto:");
+	 * assertEquals("email@example.com", result);
+	 * 
+	 * result = StringUtils.afterPrefixIgnoreCase("http://www.google.com", "mailto:");
+	 * assertNull(result);
+	 * 
+ * + * @param string the string + * @param prefix the prefix + * @return the string or null if the prefix was not found + */ + public static String afterPrefixIgnoreCase(String string, String prefix) { + if (string.length() < prefix.length()) { + return null; + } + + for (int i = 0; i < prefix.length(); i++) { + char a = Character.toUpperCase(prefix.charAt(i)); + char b = Character.toUpperCase(string.charAt(i)); + if (a != b) { + return null; + } + } + + return string.substring(prefix.length()); + } + + /** + * Creates a string consisting of "count" occurrences of char "c". + * @param c the character to repeat + * @param count the number of times to repeat the character + * @param sb the character sequence to append the characters to + */ + public static void repeat(char c, int count, StringBuilder sb) { + for (int i = 0; i < count; i++) { + sb.append(c); + } + } + + /** + * Joins a collection of values into a delimited list. + * @param collection the collection of values + * @param delimiter the delimiter (e.g. ",") + * @param the value class + * @return the final string + */ + public static String join(Collection collection, String delimiter) { + StringBuilder sb = new StringBuilder(); + join(collection, delimiter, sb); + return sb.toString(); + } + + /** + * Joins a collection of values into a delimited list. + * @param collection the collection of values + * @param delimiter the delimiter (e.g. ",") + * @param sb the string builder to append onto + * @param the value class + */ + public static void join(Collection collection, String delimiter, StringBuilder sb) { + join(collection, delimiter, sb, new JoinCallback() { + public void handle(StringBuilder sb, T value) { + sb.append(value); + } + }); + } + + /** + * Joins a collection of values into a delimited list. + * @param collection the collection of values + * @param delimiter the delimiter (e.g. ",") + * @param join callback function to call on every element in the collection + * @param the value class + * @return the final string + */ + public static String join(Collection collection, String delimiter, JoinCallback join) { + StringBuilder sb = new StringBuilder(); + join(collection, delimiter, sb, join); + return sb.toString(); + } + + /** + * Joins a collection of values into a delimited list. + * @param collection the collection of values + * @param delimiter the delimiter (e.g. ",") + * @param sb the string builder to append onto + * @param join callback function to call on every element in the collection + * @param the value class + */ + public static void join(Collection collection, String delimiter, StringBuilder sb, JoinCallback join) { + boolean first = true; + for (T element : collection) { + if (!first) { + sb.append(delimiter); + } + + join.handle(sb, element); + first = false; + } + } + + /** + * Callback interface used with various {@code StringUtils.join()} methods. + * @author Michael Angstadt + * @param the value class + */ + public interface JoinCallback { + void handle(StringBuilder sb, T value); + } + + private StringUtils() { + //hide + } +} diff --git a/app/src/main/java/biweekly/util/UtcOffset.java b/app/src/main/java/biweekly/util/UtcOffset.java new file mode 100644 index 0000000000..9098f0deb3 --- /dev/null +++ b/app/src/main/java/biweekly/util/UtcOffset.java @@ -0,0 +1,183 @@ +package biweekly.util; + +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import biweekly.Messages; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a UTC offset. + * @author Michael Angstadt + */ +public final class UtcOffset { + private final long millis; + + /** + * @param positive true if the offset is positive, false if it is negative + * @param hour the hour component of the offset (the sign of this integer is + * ignored) + * @param minute the minute component of the offset (the sign of this + * integer is ignored) + */ + public UtcOffset(boolean positive, int hour, int minute) { + //Note: The (hour, minute) constructor was removed because it could not handle timezones such as "-0030" + int sign = positive ? 1 : -1; + hour = Math.abs(hour); + minute = Math.abs(minute); + + millis = sign * (hoursToMillis(hour) + minutesToMillis(minute)); + } + + /** + * @param millis the offset in milliseconds + */ + public UtcOffset(long millis) { + this.millis = millis; + } + + /** + * Parses a UTC offset from a string. + * @param text the text to parse (e.g. "-0500") + * @return the parsed UTC offset + * @throws IllegalArgumentException if the text cannot be parsed + */ + public static UtcOffset parse(String text) { + Pattern timeZoneRegex = Pattern.compile("^([-\\+])?(\\d{1,2})(:?(\\d{2}))?(:?(\\d{2}))?$"); + Matcher m = timeZoneRegex.matcher(text); + + if (!m.find()) { + throw Messages.INSTANCE.getIllegalArgumentException(21, text); + } + + String signStr = m.group(1); + boolean positive = !"-".equals(signStr); + + String hourStr = m.group(2); + int hourOffset = Integer.parseInt(hourStr); + + String minuteStr = m.group(4); + int minuteOffset = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr); + + return new UtcOffset(positive, hourOffset, minuteOffset); + } + + /** + * Creates a UTC offset from a {@link TimeZone} object. + * @param timezone the timezone + * @return the UTC offset + */ + public static UtcOffset parse(TimeZone timezone) { + long offset = timezone.getOffset(System.currentTimeMillis()); + return new UtcOffset(offset); + } + + /** + * Gets the offset in milliseconds. + * @return the offset in milliseconds + */ + public long getMillis() { + return millis; + } + + /** + * Converts this offset to its ISO string representation using "basic" + * format. + * @return the ISO string representation (e.g. "-0500") + */ + @Override + public String toString() { + return toString(false); + } + + /** + * Converts this offset to its ISO string representation. + * @param extended true to use extended format (e.g. "-05:00"), false to use + * basic format (e.g. "-0500") + * @return the ISO string representation + */ + public String toString(boolean extended) { + StringBuilder sb = new StringBuilder(); + + boolean positive = (millis >= 0); + long hour = Math.abs(millisToHours(millis)); + long minute = Math.abs(millisToMinutes(millis)); + + sb.append(positive ? '+' : '-'); + + if (hour < 10) { + sb.append('0'); + } + sb.append(hour); + + if (extended) { + sb.append(':'); + } + + if (minute < 10) { + sb.append('0'); + } + sb.append(minute); + + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (millis ^ (millis >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + UtcOffset other = (UtcOffset) obj; + if (millis != other.millis) return false; + return true; + } + + private static long hoursToMillis(long hours) { + return hours * 60 * 60 * 1000; + } + + private static long minutesToMillis(long minutes) { + return minutes * 60 * 1000; + } + + private static long millisToHours(long millis) { + return millis / 1000 / 60 / 60; + } + + private static long millisToMinutes(long millis) { + return (millis / 1000 / 60) % 60; + } +} diff --git a/app/src/main/java/biweekly/util/Utf8Reader.java b/app/src/main/java/biweekly/util/Utf8Reader.java new file mode 100644 index 0000000000..3f1fd84a00 --- /dev/null +++ b/app/src/main/java/biweekly/util/Utf8Reader.java @@ -0,0 +1,57 @@ +package biweekly.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Reads characters that are encoded in UTF-8. + * @author Michael Angstadt + */ +public class Utf8Reader extends InputStreamReader { + /** + * Creates a new UTF-8 reader. + * @param in the input stream to read from + */ + public Utf8Reader(InputStream in) { + super(in, Charset.forName("UTF-8")); + } + + /** + * Creates a new UTF-8 reader. + * @param file the file to read from + * @throws FileNotFoundException if the file does not exist or cannot be + * opened + */ + public Utf8Reader(File file) throws FileNotFoundException { + this(new FileInputStream(file)); + } +} diff --git a/app/src/main/java/biweekly/util/Utf8Writer.java b/app/src/main/java/biweekly/util/Utf8Writer.java new file mode 100644 index 0000000000..a93cfdb100 --- /dev/null +++ b/app/src/main/java/biweekly/util/Utf8Writer.java @@ -0,0 +1,67 @@ +package biweekly.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Writes characters that are UTF-8 encoded. + * @author Michael Angstadt + */ +public class Utf8Writer extends OutputStreamWriter { + /** + * Creates a new UTF-8 writer. + * @param out the output stream to write to + */ + public Utf8Writer(OutputStream out) { + super(out, Charset.forName("UTF-8")); + } + + /** + * Creates a new UTF-8 writer. + * @param file the file to write to + * @throws FileNotFoundException if the file cannot be written to + */ + public Utf8Writer(File file) throws FileNotFoundException { + this(file, false); + } + + /** + * Creates a new UTF-8 writer. + * @param file the file to write to + * @param append true to append to the file, false to overwrite it (this + * parameter has no effect if the file does not exist) + * @throws FileNotFoundException if the file cannot be written to + */ + public Utf8Writer(File file, boolean append) throws FileNotFoundException { + this(new FileOutputStream(file, append)); + } +} diff --git a/app/src/main/java/biweekly/util/VersionNumber.java b/app/src/main/java/biweekly/util/VersionNumber.java new file mode 100644 index 0000000000..e454f6cf19 --- /dev/null +++ b/app/src/main/java/biweekly/util/VersionNumber.java @@ -0,0 +1,103 @@ +package biweekly.util; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a software version number (e.g. "1.8.14"). + * @author Michael Angstadt + */ +public class VersionNumber implements Comparable { + private final List parts; + + /** + * Creates a new version number. + * @param version the version string (e.g. "1.8.14") + * @throws IllegalArgumentException if the version string is invalid + */ + public VersionNumber(String version) { + parts = new ArrayList(); + + int start = 0; + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + if (c == '.') { + addNumber(version, start, i); + start = i + 1; + } + } + addNumber(version, start, version.length()); + } + + private void addNumber(String version, int fromIndex, int toIndex) { + String numberStr = version.substring(fromIndex, toIndex); + Integer number = Integer.valueOf(numberStr); + parts.add(number); + } + + public int compareTo(VersionNumber that) { + Iterator it = parts.iterator(); + Iterator it2 = that.parts.iterator(); + while (it.hasNext() || it2.hasNext()) { + int number = it.hasNext() ? it.next() : 0; + int number2 = it2.hasNext() ? it2.next() : 0; + + if (number < number2) { + return -1; + } + if (number > number2) { + return 1; + } + } + return 0; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + parts.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + VersionNumber other = (VersionNumber) obj; + if (!parts.equals(other.parts)) return false; + return true; + } + + @Override + public String toString() { + return StringUtils.join(parts, "."); + } +} diff --git a/app/src/main/java/biweekly/util/XmlUtils.java b/app/src/main/java/biweekly/util/XmlUtils.java new file mode 100644 index 0000000000..e2188d079d --- /dev/null +++ b/app/src/main/java/biweekly/util/XmlUtils.java @@ -0,0 +1,359 @@ +package biweekly.util; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The views and conclusions contained in the software and documentation are those + of the authors and should not be interpreted as representing official policies, + either expressed or implied, of the FreeBSD Project. + */ + +/** + * Generic XML utility methods. + * @author Michael Angstadt + */ +public final class XmlUtils { + /** + * Creates a new XML document. + * @return the XML document + */ + public static Document createDocument() { + try { + DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); + fact.setNamespaceAware(true); + DocumentBuilder db = fact.newDocumentBuilder(); + return db.newDocument(); + } catch (ParserConfigurationException e) { + //will probably never be thrown because we're not doing anything fancy with the configuration + throw new RuntimeException(e); + } + } + + /** + * Parses an XML string into a DOM. + * @param xml the XML string + * @return the parsed DOM + * @throws SAXException if the string is not valid XML + */ + public static Document toDocument(String xml) throws SAXException { + try { + return toDocument(new StringReader(xml)); + } catch (IOException e) { + //reading from string + throw new RuntimeException(e); + } + } + + /** + * Parses an XML document from a file. + * @param file the file + * @return the parsed DOM + * @throws SAXException if the XML is not valid + * @throws IOException if there is a problem reading from the file + */ + public static Document toDocument(File file) throws SAXException, IOException { + InputStream in = new BufferedInputStream(new FileInputStream(file)); + try { + return XmlUtils.toDocument(in); + } finally { + in.close(); + } + } + + /** + * Parses an XML document from an input stream. + * @param in the input stream + * @return the parsed DOM + * @throws SAXException if the XML is not valid + * @throws IOException if there is a problem reading from the input stream + */ + public static Document toDocument(InputStream in) throws SAXException, IOException { + return toDocument(new InputSource(in)); + } + + /** + *

+ * Parses an XML document from a reader. + *

+ *

+ * Note that use of this method is discouraged. It ignores the character + * encoding that is defined within the XML document itself, and should only + * be used if the encoding is undefined or if the encoding needs to be + * ignored for whatever reason. The {@link #toDocument(InputStream)} method + * should be used instead, since it takes the XML document's character + * encoding into account when parsing. + *

+ * @param reader the reader + * @return the parsed DOM + * @throws SAXException if the XML is not valid + * @throws IOException if there is a problem reading from the reader + * @see http://stackoverflow.com/q/3482494/13379 + */ + public static Document toDocument(Reader reader) throws SAXException, IOException { + return toDocument(new InputSource(reader)); + } + + private static Document toDocument(InputSource in) throws SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setIgnoringComments(true); + applyXXEProtection(factory); + + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + //should never be thrown because we're not doing anything fancy with the configuration + throw new RuntimeException(e); + } + + return builder.parse(in); + } + + /** + * Configures a {@link DocumentBuilderFactory} to protect it against XML + * External Entity attacks. + * @param factory the factory + * @see + * XXE Cheat Sheet + */ + public static void applyXXEProtection(DocumentBuilderFactory factory) { + Map features = new HashMap(); + features.put("http://apache.org/xml/features/disallow-doctype-decl", true); + features.put("http://xml.org/sax/features/external-general-entities", false); + features.put("http://xml.org/sax/features/external-parameter-entities", false); + features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + for (Map.Entry entry : features.entrySet()) { + String feature = entry.getKey(); + Boolean value = entry.getValue(); + try { + factory.setFeature(feature, value); + } catch (ParserConfigurationException e) { + //feature is not supported by the local XML engine, skip it + } + } + + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + } + + /** + * Configures a {@link TransformerFactory} to protect it against XML + * External Entity attacks. + * @param factory the factory + * @see + * XXE Cheat Sheet + */ + public static void applyXXEProtection(TransformerFactory factory) { + //@formatter:off + String[] attributes = { + //XMLConstants.ACCESS_EXTERNAL_DTD (Java 7 only) + "http://javax.xml.XMLConstants/property/accessExternalDTD", + + //XMLConstants.ACCESS_EXTERNAL_STYLESHEET (Java 7 only) + "http://javax.xml.XMLConstants/property/accessExternalStylesheet" + }; + //@formatter:on + + for (String attribute : attributes) { + try { + factory.setAttribute(attribute, ""); + } catch (IllegalArgumentException e) { + //attribute is not supported by the local XML engine, skip it + } + } + } + + /** + * Converts an XML node to a string. + * @param node the XML node + * @return the string + */ + public static String toString(Node node) { + return toString(node, new HashMap()); + } + + /** + * Converts an XML node to a string. + * @param node the XML node + * @param prettyPrint true to pretty print, false not to + * @return the string + */ + public static String toString(Node node, boolean prettyPrint) { + Map properties = new HashMap(); + if (prettyPrint) { + properties.put(OutputKeys.INDENT, "yes"); + properties.put("{http://xml.apache.org/xslt}indent-amount", "2"); + } + return toString(node, properties); + } + + /** + * Converts an XML node to a string. + * @param node the XML node + * @param outputProperties the output properties + * @return the string + */ + public static String toString(Node node, Map outputProperties) { + try { + StringWriter writer = new StringWriter(); + toWriter(node, writer, outputProperties); + return writer.toString(); + } catch (TransformerException e) { + //should never be thrown because we're writing to string + throw new RuntimeException(e); + } + } + + /** + * Writes an XML node to a writer. + * @param node the XML node + * @param writer the writer + * @throws TransformerException if there's a problem writing to the writer + */ + public static void toWriter(Node node, Writer writer) throws TransformerException { + toWriter(node, writer, new HashMap()); + } + + /** + * Writes an XML node to a writer. + * @param node the XML node + * @param writer the writer + * @param outputProperties the output properties + * @throws TransformerException if there's a problem writing to the writer + */ + public static void toWriter(Node node, Writer writer, Map outputProperties) throws TransformerException { + try { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + for (Map.Entry property : outputProperties.entrySet()) { + try { + transformer.setOutputProperty(property.getKey(), property.getValue()); + } catch (IllegalArgumentException e) { + //ignore invalid output properties + } + } + + DOMSource source = new DOMSource(node); + StreamResult result = new StreamResult(writer); + transformer.transform(source, result); + } catch (TransformerConfigurationException e) { + //no complex configurations + } catch (TransformerFactoryConfigurationError e) { + //no complex configurations + } + } + + /** + * Gets all the elements out of a {@link NodeList}. + * @param nodeList the node list + * @return the elements + */ + public static List toElementList(NodeList nodeList) { + List elements = new ArrayList(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node instanceof Element) { + elements.add((Element) node); + } + } + return elements; + } + + /** + * Gets the first child element of an element. + * @param parent the parent element + * @return the first child element or null if there are no child elements + */ + public static Element getFirstChildElement(Element parent) { + return getFirstChildElement((Node) parent); + } + + /** + * Gets the first child element of a node. + * @param parent the node + * @return the first child element or null if there are no child elements + */ + private static Element getFirstChildElement(Node parent) { + NodeList nodeList = parent.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node instanceof Element) { + return (Element) node; + } + } + return null; + } + + /** + * Determines if a node has a particular qualified name. + * @param node the node + * @param qname the qualified name + * @return true if the node has the given qualified name, false if not + */ + public static boolean hasQName(Node node, QName qname) { + return qname.getNamespaceURI().equals(node.getNamespaceURI()) && qname.getLocalPart().equals(node.getLocalName()); + } + + private XmlUtils() { + //hide + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIterable.java b/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIterable.java new file mode 100644 index 0000000000..7e8abb5f34 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIterable.java @@ -0,0 +1,51 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.compat.javautil; + +import java.util.Date; + +/** + * Iterates over a series of {@link Date} objects in ascending order. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public interface DateIterable extends Iterable { + DateIterator iterator(); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIterator.java b/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIterator.java new file mode 100644 index 0000000000..0af62ca201 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIterator.java @@ -0,0 +1,56 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.compat.javautil; + +import java.util.Date; +import java.util.Iterator; + +/** + * Iterates over a series of {@link Date} objects in ascending order. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public interface DateIterator extends Iterator { + /** + * Skips all dates in the series that come before the given date. + * @param newStartUtc the date to advance to (in UTC) + */ + void advanceTo(Date newStartUtc); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIteratorFactory.java b/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIteratorFactory.java new file mode 100644 index 0000000000..095796413d --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/compat/javautil/DateIteratorFactory.java @@ -0,0 +1,169 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.compat.javautil; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import biweekly.util.com.google.ical.iter.RecurrenceIterable; +import biweekly.util.com.google.ical.iter.RecurrenceIterator; +import biweekly.util.com.google.ical.iter.RecurrenceIteratorFactory; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateTimeValueImpl; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + * A factory for converting RRULEs and RDATEs into + * Iterator<Date> and Iterable<Date>. + * @see RecurrenceIteratorFactory + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public class DateIteratorFactory { + /** + * Creates a date iterator from a recurrence iterator. + * @param rit the recurrence iterator + * @return the date iterator + */ + public static DateIterator createDateIterator(RecurrenceIterator rit) { + return new RecurrenceIteratorWrapper(rit); + } + + /** + * Creates a date iterable from a recurrence iterable. + * @param rit the recurrence iterable + * @return the date iterable + */ + public static DateIterable createDateIterable(RecurrenceIterable rit) { + return new RecurrenceIterableWrapper(rit); + } + + private static final class RecurrenceIterableWrapper implements DateIterable { + private final RecurrenceIterable it; + + public RecurrenceIterableWrapper(RecurrenceIterable it) { + this.it = it; + } + + public DateIterator iterator() { + return new RecurrenceIteratorWrapper(it.iterator()); + } + } + + private static final class RecurrenceIteratorWrapper implements DateIterator { + private final RecurrenceIterator it; + private final Calendar utcCalendar = new GregorianCalendar(TimeUtils.utcTimezone()); + + public RecurrenceIteratorWrapper(RecurrenceIterator it) { + this.it = it; + } + + public boolean hasNext() { + return it.hasNext(); + } + + public Date next() { + return toDate(it.next()); + } + + public void advanceTo(Date d) { + it.advanceTo(toDateValue(d)); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Converts a {@link DateValue} object into a Java {@link Date} object. + * @param dateValue the date value object (assumed to be in UTC time) + * @return the Java date object + */ + private Date toDate(DateValue dateValue) { + TimeValue time = TimeUtils.timeOf(dateValue); + utcCalendar.clear(); + //@formatter:off + utcCalendar.set( + dateValue.year(), + dateValue.month() - 1, //java.util's dates are zero-indexed + dateValue.day(), + time.hour(), + time.minute(), + time.second() + ); + //@formatter:on + return utcCalendar.getTime(); + } + + /** + * Converts a Java {@link Date} object into a {@link DateValue} object. + * The {@link DateValue} object will be in UTC time. + * @param date the Java date object + * @return the date value object (in UTC time) + */ + private DateValue toDateValue(Date date) { + utcCalendar.setTime(date); + + int year = utcCalendar.get(Calendar.YEAR); + int month = utcCalendar.get(Calendar.MONTH) + 1; //java.util's dates are zero-indexed + int day = utcCalendar.get(Calendar.DAY_OF_MONTH); + int hour = utcCalendar.get(Calendar.HOUR_OF_DAY); + int minute = utcCalendar.get(Calendar.MINUTE); + int second = utcCalendar.get(Calendar.SECOND); + + /* + * We need to treat midnight as a date value so that passing in + * dateValueToDate() will not advance past any + * occurrences of some-date-value in the iterator. + */ + if ((hour | minute | second) == 0) { + return new DateValueImpl(year, month, day); + } + return new DateTimeValueImpl(year, month, day, hour, minute, second); + } + } + + private DateIteratorFactory() { + // uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/CompoundIteratorImpl.java b/app/src/main/java/biweekly/util/com/google/ical/iter/CompoundIteratorImpl.java new file mode 100644 index 0000000000..47de263c83 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/CompoundIteratorImpl.java @@ -0,0 +1,289 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.Collection; +import java.util.Comparator; +import java.util.NoSuchElementException; +import java.util.PriorityQueue; + +import biweekly.util.com.google.ical.values.DateValue; + +/** + * A recurrence iterator that combines multiple recurrence iterators into one. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class CompoundIteratorImpl implements RecurrenceIterator { + /** + * A queue that keeps the earliest dates at the head. + */ + private final PriorityQueue queue; + + private HeapElement pending; + + /** + * The number of inclusions on the queue. We keep track of this so that we + * don't have to drain the exclusions to conclude that the series is + * exhausted. + */ + private int nInclusionsRemaining; + + /** + * Creates an iterator that will generate only dates that are generated by + * inclusions and will not generate any dates that are generated by + * exclusions. + * @param inclusions iterators whose elements should be included unless + * explicitly excluded + * @param exclusions iterators whose elements should not be included + */ + CompoundIteratorImpl(Collection inclusions, Collection exclusions) { + queue = new PriorityQueue(inclusions.size() + exclusions.size(), HeapElement.CMP); + for (RecurrenceIterator it : inclusions) { + HeapElement el = new HeapElement(true, it); + if (el.shift()) { + queue.add(el); + ++nInclusionsRemaining; + } + } + for (RecurrenceIterator it : exclusions) { + HeapElement el = new HeapElement(false, it); + if (el.shift()) { + queue.add(el); + } + } + } + + public boolean hasNext() { + requirePending(); + return pending != null; + } + + public DateValue next() { + requirePending(); + if (pending == null) { + throw new NoSuchElementException(); + } + DateValue head = pending.head(); + reattach(pending); + pending = null; + return head; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void advanceTo(DateValue newStart) { + long newStartCmp = DateValueComparison.comparable(newStart); + if (pending != null) { + if (pending.comparable() >= newStartCmp) { + return; + } + pending.advanceTo(newStart); + reattach(pending); + pending = null; + } + + /* + * Pull each element off the stack in turn, and advance it. Once we + * reach one we don't need to advance, we're done. + */ + while (nInclusionsRemaining != 0 && !queue.isEmpty() && queue.peek().comparable() < newStartCmp) { + HeapElement el = queue.poll(); + el.advanceTo(newStart); + reattach(el); + } + } + + /** + * If the given element's iterator has more data, then push back onto the + * heap. + * @param el the element to push back into the heap. + */ + private void reattach(HeapElement el) { + if (el.shift()) { + queue.add(el); + } else if (el.inclusion) { + /* + * If we have no live inclusions, then the rest are exclusions which + * we can safely discard. + */ + if (--nInclusionsRemaining == 0) { + queue.clear(); + } + } + } + + /** + * Makes sure that pending contains the next inclusive {@link HeapElement} + * that doesn't match any exclusion, and remove any duplicates of it. + */ + private void requirePending() { + if (pending != null) { + return; + } + + long exclusionComparable = Long.MIN_VALUE; + while (nInclusionsRemaining != 0 && !queue.isEmpty()) { + //find a candidate that is not excluded + HeapElement inclusion = null; + do { + HeapElement candidate = queue.poll(); + if (candidate.inclusion) { + if (exclusionComparable != candidate.comparable()) { + inclusion = candidate; + break; + } + } else { + exclusionComparable = candidate.comparable(); + } + reattach(candidate); + if (nInclusionsRemaining == 0) { + return; + } + } while (!queue.isEmpty()); + if (inclusion == null) { + return; + } + long inclusionComparable = inclusion.comparable(); + + /* + * Check for any following exclusions and for duplicates. We could + * change the sort order so that exclusions always preceded + * inclusions, but that would be less efficient and would make the + * ordering different than the comparable value. + */ + boolean excluded = exclusionComparable == inclusionComparable; + while (!queue.isEmpty() && queue.peek().comparable() == inclusionComparable) { + HeapElement match = queue.poll(); + excluded |= !match.inclusion; + reattach(match); + if (nInclusionsRemaining == 0) { + return; + } + } + if (!excluded) { + pending = inclusion; + return; + } + reattach(inclusion); + } + } +} + +final class HeapElement { + /** + * Should iterators items be included in the series or should they nullify + * any matched items included by other series? + */ + final boolean inclusion; + + private final RecurrenceIterator it; + + /** + * The {@link DateValueComparison#comparable} for {@link #head}. + */ + private long comparable; + + /** + * The last value removed from the iterator (in UTC). + */ + private DateValue head; + + HeapElement(boolean inclusion, RecurrenceIterator it) { + this.inclusion = inclusion; + this.it = it; + } + + /** + * Gets the last value removed from the iterator. + */ + DateValue head() { + return head; + } + + /** + * Gets the comparable value of the head. A given HeapElement may be + * compared to many others as it bubbles towards the heap's root, so we + * cache this for each HeapElement. + * @return the comparable value of the head + */ + long comparable() { + return comparable; + } + + /** + * Discards the current element and move to the next. + * @return true if there is a next element, false if not + */ + boolean shift() { + if (!it.hasNext()) { + return false; + } + head = it.next(); + comparable = DateValueComparison.comparable(head); + return true; + } + + /** + * Advances the underlying iterator to the given date value. + * @param newStartUtc the date to advance to (in UTC) + * @see RecurrenceIterator#advanceTo + */ + void advanceTo(DateValue newStartUtc) { + it.advanceTo(newStartUtc); + } + + @Override + public String toString() { + return "[" + head.toString() + ", " + (inclusion ? "inclusion" : "exclusion") + "]"; + } + + /** + * Compares two heap elements by comparing their heads. + */ + static final Comparator CMP = new Comparator() { + public int compare(HeapElement a, HeapElement b) { + long ac = a.comparable(), bc = b.comparable(); + return ac < bc ? -1 : ac == bc ? 0 : 1; + } + }; +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/Conditions.java b/app/src/main/java/biweekly/util/com/google/ical/iter/Conditions.java new file mode 100644 index 0000000000..dafb764dbb --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/Conditions.java @@ -0,0 +1,97 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import biweekly.util.com.google.ical.util.Predicate; +import biweekly.util.com.google.ical.values.DateValue; + +/** + * Factory for predicates used to test whether a recurrence is over. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class Conditions { + /** + * Constructs a condition that fails after counting a certain number of + * dates. + * @param count the number of dates to count before the condition fails + * @return the condition + */ + static Predicate countCondition(final int count) { + return new Predicate() { + private static final long serialVersionUID = -3770774958208833665L; + int count_ = count; + + public boolean apply(DateValue value) { + return --count_ >= 0; + } + + @Override + public String toString() { + return "CountCondition:" + count_; + } + }; + } + + /** + * Constructs a condition that passes all dates that are less than or equal + * to the given date. + * @param until the date + * @return the condition + */ + static Predicate untilCondition(final DateValue until) { + return new Predicate() { + private static final long serialVersionUID = -130394842437801858L; + + public boolean apply(DateValue date) { + return date.compareTo(until) <= 0; + } + + @Override + public String toString() { + return "UntilCondition:" + until; + } + }; + } + + private Conditions() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/DateValueComparison.java b/app/src/main/java/biweekly/util/com/google/ical/iter/DateValueComparison.java new file mode 100644 index 0000000000..c61da30aae --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/DateValueComparison.java @@ -0,0 +1,128 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + *

+ * Contains {@link DateValue} comparison methods. + *

+ *

+ * When we're pulling dates off the priority order, we need them to come off in + * a consistent order, so we need a total ordering on date values. + *

+ *

+ * This means that a DateValue with no time must not be equal to a DateTimeValue + * at midnight. Since it obviously doesn't make sense for a DateValue to be + * after a DateTimeValue the same day at 23:59:59, we put the DateValue before 0 + * hours of the same day. + *

+ *

+ * If we didn't have a total ordering, then it would be harder to correctly + * handle the example below because we'd have two EXDATEs that are equal + * according to the comparison, but only the first should match. + *

+ * + *
+ *   RDATE:20060607
+ *   EXDATE:20060607
+ *   EXDATE:20060607T000000Z
+ * 
+ *

+ * In the next example, the problem is worse because we may pull a candidate + * RDATE off the priority queue and then not know whether to consume the EXDATE + * or not. + *

+ * + *
+ *   RDATE:20060607
+ *   RDATE:20060607T000000Z
+ *   EXDATE:20060607
+ * 
+ *

+ * Absent a total ordering, the following case could only be solved with + * lookahead and ugly logic. + *

+ * + *
+ *   RDATE:20060607
+ *   RDATE:20060607T000000Z
+ *   EXDATE:20060607
+ *   EXDATE:20060607T000000Z
+ * 
+ *

+ * The conversion to GMT is also an implementation detail, so it's not clear + * which timezone we should consider midnight in, and a total ordering allows us + * to avoid timezone conversions during iteration. + *

+ * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class DateValueComparison { + /** + * Reduces a date to a value that can be easily compared to others, consistent + * with {@link DateValueImpl#compareTo}. + * @param date the date + * @return the value to use for comparisons + */ + static long comparable(DateValue date) { + long comp = (((((long) date.year()) << 4) + date.month()) << 5) + date.day(); + if (date instanceof TimeValue) { + TimeValue time = (TimeValue) date; + + /* + * We add 1 to comparable for timed values to make sure that timed events + * are distinct from all-day events, in keeping with DateValue.compareTo. + * + * It would be odd if an all day exclusion matched a midnight event on the + * same day, but not one at another time of day. + */ + return (((((comp << 5) + time.hour()) << 6) + time.minute()) << 6) + time.second() + 1; + } + return comp << 17; + } + + private DateValueComparison() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/Filters.java b/app/src/main/java/biweekly/util/com/google/ical/iter/Filters.java new file mode 100644 index 0000000000..8f7869418e --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/Filters.java @@ -0,0 +1,290 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import biweekly.util.ByDay; +import biweekly.util.DayOfWeek; +import biweekly.util.com.google.ical.util.DTBuilder; +import biweekly.util.com.google.ical.util.Predicate; +import biweekly.util.com.google.ical.util.Predicates; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + *

+ * Factory for creating predicates used to filter out dates produced by a + * generator that do not pass some secondary criterion. For example, the + * recurrence rule below should generate every Friday the 13th: + *

+ * + *
+ * FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
+ * 
+ *

+ * It is implemented as a generator that generates the 13th of every month (a + * {@code byMonthDay} generator), and then the results of that are filtered by a + * {@code byDayFilter} that tests whether the date falls on Friday. + *

+ *

+ * A filter returns true to indicate the item is included in the recurrence. + *

+ * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +class Filters { + /** + * Constructs a day filter based on a BYDAY rule. + * @param days the BYDAY values + * @param weeksInYear true if the week numbers are meant to be weeks in the + * current year, false if they are meant to be weeks in the current month + * @param weekStart the day of the week that the week starts on + * @return the filter + */ + static Predicate byDayFilter(final ByDay[] days, final boolean weeksInYear, final DayOfWeek weekStart) { + return new Predicate() { + private static final long serialVersionUID = 1636822853835207274L; + + public boolean apply(DateValue date) { + DayOfWeek dow = TimeUtils.dayOfWeek(date); + int nDays; + DayOfWeek firstDayOfWeek; + + //where does date appear in the year or month? + //in [0, lengthOfMonthOrYear - 1] + int instance; + if (weeksInYear) { + nDays = TimeUtils.yearLength(date.year()); + firstDayOfWeek = TimeUtils.firstDayOfWeekInMonth(date.year(), 1); + instance = TimeUtils.dayOfYear(date.year(), date.month(), date.day()); + } else { + nDays = TimeUtils.monthLength(date.year(), date.month()); + firstDayOfWeek = TimeUtils.firstDayOfWeekInMonth(date.year(), date.month()); + instance = date.day() - 1; + } + + //which week of the year or month does this date fall on? + //one-indexed + int dateWeekNo = instance / 7; + if (weekStart.getCalendarConstant() <= dow.getCalendarConstant()) { + dateWeekNo += 1; + } + + /* + * TODO(msamuel): According to section 4.3.10: + * + * Week number one of the calendar year is the first week which + * contains at least four (4) days in that calendar year. This + * rule part is only valid for YEARLY rules. + * + * That's mentioned under the BYWEEKNO rule, and there's no + * mention of it in the earlier discussion of the BYDAY rule. + * Does it apply to yearly week numbers calculated for BYDAY + * rules in a FREQ=YEARLY rule? + */ + + for (int i = days.length - 1; i >= 0; i--) { + ByDay day = days[i]; + + if (day.getDay() == dow) { + Integer weekNo = day.getNum(); + if (weekNo == null || weekNo == 0) { + return true; + } + + if (weekNo < 0) { + weekNo = Util.invertWeekdayNum(day, firstDayOfWeek, nDays); + } + + if (dateWeekNo == weekNo) { + return true; + } + } + } + return false; + } + }; + } + + /** + * Constructs a day filter based on a BYDAY rule. + * @param monthDays days of the month (values must be in range [-31,31]) + * @return the filter + */ + static Predicate byMonthDayFilter(final int[] monthDays) { + return new Predicate() { + private static final long serialVersionUID = -1618039447294490037L; + + public boolean apply(DateValue date) { + int nDays = TimeUtils.monthLength(date.year(), date.month()); + for (int i = monthDays.length - 1; i >= 0; i--) { + int day = monthDays[i]; + if (day < 0) { + day += nDays + 1; + } + if (day == date.day()) { + return true; + } + } + return false; + } + }; + } + + /** + * Constructs a filter that accepts only every X week starting from the week + * containing the given date. + * @param interval the interval (for example, 3 for "every third week"; must + * be > 0) + * @param weekStart the day of the week that the week starts on + * @param dtStart the filter will start at the week that contains this date + * @return the filter + */ + static Predicate weekIntervalFilter(final int interval, final DayOfWeek weekStart, final DateValue dtStart) { + return new Predicate() { + private static final long serialVersionUID = 7059994888520369846L; + //the latest day with day of week weekStart on or before dtStart + DateValue wkStart; + { + DTBuilder wkStartB = new DTBuilder(dtStart); + wkStartB.day -= (7 + TimeUtils.dayOfWeek(dtStart).getCalendarConstant() - weekStart.getCalendarConstant()) % 7; + wkStart = wkStartB.toDate(); + } + + public boolean apply(DateValue date) { + int daysBetween = TimeUtils.daysBetween(date, wkStart); + if (daysBetween < 0) { + //date must be before dtStart. Shouldn't occur in practice. + daysBetween += (interval * 7 * (1 + daysBetween / (-7 * interval))); + } + int off = (daysBetween / 7) % interval; + return off == 0; + } + }; + } + + private static final int LOW_24_BITS = ~(-1 << 24); + private static final long LOW_60_BITS = ~(-1L << 60); + + /** + * Constructs an hour filter based on a BYHOUR rule. + * @param hours hours of the day (values must be in range [0,23]) + * @return the filter + */ + static Predicate byHourFilter(int[] hours) { + int hoursByBit = 0; + for (int hour : hours) { + hoursByBit |= 1 << hour; + } + if ((hoursByBit & LOW_24_BITS) == LOW_24_BITS) { + return Predicates.alwaysTrue(); + } + final int bitField = hoursByBit; + return new Predicate() { + private static final long serialVersionUID = -6284974028385246889L; + + public boolean apply(DateValue date) { + if (!(date instanceof TimeValue)) { + return false; + } + TimeValue tv = (TimeValue) date; + return (bitField & (1 << tv.hour())) != 0; + } + }; + } + + /** + * Constructs a minute filter based on a BYMINUTE rule. + * @param minutes minutes of the hour (values must be in range [0,59]) + * @return the filter + */ + static Predicate byMinuteFilter(int[] minutes) { + long minutesByBit = 0; + for (int minute : minutes) { + minutesByBit |= 1L << minute; + } + if ((minutesByBit & LOW_60_BITS) == LOW_60_BITS) { + return Predicates.alwaysTrue(); + } + final long bitField = minutesByBit; + return new Predicate() { + private static final long serialVersionUID = 5028303473420393470L; + + public boolean apply(DateValue date) { + if (!(date instanceof TimeValue)) { + return false; + } + TimeValue tv = (TimeValue) date; + return (bitField & (1L << tv.minute())) != 0; + } + }; + } + + /** + * Constructs a second filter based on a BYMINUTE rule. + * @param seconds seconds of the minute (values must be in rage [0,59]) + * @return the filter + */ + static Predicate bySecondFilter(int[] seconds) { + long secondsByBit = 0; + for (int second : seconds) { + secondsByBit |= 1L << second; + } + if ((secondsByBit & LOW_60_BITS) == LOW_60_BITS) { + return Predicates.alwaysTrue(); + } + final long bitField = secondsByBit; + return new Predicate() { + private static final long serialVersionUID = 4109739845053177924L; + + public boolean apply(DateValue date) { + if (!(date instanceof TimeValue)) { + return false; + } + TimeValue tv = (TimeValue) date; + return (bitField & (1L << tv.second())) != 0; + } + }; + } + + private Filters() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/Generator.java b/app/src/main/java/biweekly/util/com/google/ical/iter/Generator.java new file mode 100644 index 0000000000..62d01d61c4 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/Generator.java @@ -0,0 +1,116 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import biweekly.util.com.google.ical.util.DTBuilder; + +/** + *

+ * A stateful operation that can be successively invoked to generate the next + * part of a date in a series. + *

+ *

+ * Each field generator takes as input the larger fields, modifies its field, + * and leaves the other fields unchanged. For example, a year generator will + * update {@link DTBuilder#year}, leaving the smaller fields unchanged. And a + * month generator will update {@link DTBuilder#month}, taking its cue from + * {@link DTBuilder#year}, also leaving the smaller fields unchanged. + *

+ * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +abstract class Generator { + /** + *

+ * Generates the next part of a date in a series. + *

+ *

+ * If a generator is exhausted, generating a new value of a larger field may + * allow it to continue. For example, a month generator that runs out of + * months at 12, may start over at 1 if called with a {@link DTBuilder} with + * a different year. + *

+ * @param bldr used for both input and output, modified in place + * @return true iff there are more instances of the generator's field to + * generate + * @throws IteratorShortCircuitingException when an iterator reaches a + * threshold past which it cannot generate any more dates. This indicates + * that the entire iteration process should end. + */ + abstract boolean generate(DTBuilder bldr) throws IteratorShortCircuitingException; + + /** + *

+ * Thrown when an iteration process should be ended completely due to an + * artificial system limit. This allows us to make a distinction between + * normal exhaustion of iteration, and an artificial limit that may fall in + * a set, and so affect subsequent evaluation of BYSETPOS rules. + *

+ *

+ * Since this class is meant to be thrown as a flow control construct to + * indicate an artificial limit has been reached (as opposed to an + * exceptional condition), and since its clients have no need of the + * stacktrace, we use a singleton to avoid forcing the JVM to unoptimize and + * decompile the {@link RecurrenceIterator}'s inner loop. + *

+ */ + @SuppressWarnings("serial") + static class IteratorShortCircuitingException extends Exception { + private IteratorShortCircuitingException() { + super(); + setStackTrace(new StackTraceElement[0]); + } + + private static final IteratorShortCircuitingException INSTANCE = new IteratorShortCircuitingException(); + + static IteratorShortCircuitingException instance() { + return INSTANCE; + } + } + + static { + /* + * Suffer the stack trace generation on class load of Generator, which + * will happen before any of the recurrence stuff could possibly have + * been JIT compiled. + */ + IteratorShortCircuitingException.instance(); + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/Generators.java b/app/src/main/java/biweekly/util/com/google/ical/iter/Generators.java new file mode 100644 index 0000000000..5fe4ef1725 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/Generators.java @@ -0,0 +1,1061 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.Arrays; + +import biweekly.util.ByDay; +import biweekly.util.DayOfWeek; +import biweekly.util.com.google.ical.util.DTBuilder; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + * Factory for field generators. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class Generators { + /** + *

+ * The maximum number of years generated between instances. See + * {@link ThrottledGenerator} for a description of the problem this solves. + *

+ *

+ * Note: This counts the maximum number of years generated, so for + * FREQ=YEARLY;INTERVAL=4 the generator would try 100 + * individual years over a span of 400 years before giving up and concluding + * that the rule generates no usable dates. + *

+ */ + private static final int MAX_YEARS_BETWEEN_INSTANCES = 100; + + /** + * Constructs a generator that generates years successively counting from + * the first year passed in. + * @param interval number of years to advance each step + * @param dtStart the start date + * @return the year in dtStart the first time called and interval + last + * return value on subsequent calls + */ + static ThrottledGenerator serialYearGenerator(final int interval, final DateValue dtStart) { + return new ThrottledGenerator() { + //the last year seen + int year = dtStart.year() - interval; + + int throttle = MAX_YEARS_BETWEEN_INSTANCES; + + @Override + boolean generate(DTBuilder builder) throws IteratorShortCircuitingException { + /* + * Make sure things halt even if the RRULE is bad. For example, + * the following rules should halt: + * + * FREQ=YEARLY;BYMONTHDAY=30;BYMONTH=2 + */ + if (--throttle < 0) { + throw IteratorShortCircuitingException.instance(); + } + year += interval; + builder.year = year; + return true; + } + + @Override + void workDone() { + this.throttle = MAX_YEARS_BETWEEN_INSTANCES; + } + + @Override + public String toString() { + return "serialYearGenerator:" + interval; + } + }; + } + + /** + * Constructs a generator that generates months in the given builder's year + * successively counting from the first month passed in. + * @param interval number of months to advance each step + * @param dtStart the start date + * @return the month in dtStart the first time called and interval + last + * return value on subsequent calls + */ + static Generator serialMonthGenerator(final int interval, final DateValue dtStart) { + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month() - interval; + { + while (month < 1) { + month += 12; + --year; + } + } + + @Override + boolean generate(DTBuilder builder) { + int nmonth; + if (year != builder.year) { + int monthsBetween = (builder.year - year) * 12 - (month - 1); + nmonth = ((interval - (monthsBetween % interval)) % interval) + 1; + if (nmonth > 12) { + /* + * Don't update year so that the difference calculation + * above is correct when this function is reentered with + * a different year + */ + return false; + } + year = builder.year; + } else { + nmonth = month + interval; + if (nmonth > 12) { + return false; + } + } + month = builder.month = nmonth; + return true; + } + + @Override + public String toString() { + return "serialMonthGenerator:" + interval; + } + }; + } + + /** + * Constructs a generator that generates every day in the current month that + * is an integer multiple of interval days from dtStart. + * @param interval number of days to advance each step + * @param dtStart the start date + * @return the day in dtStart the first time called and interval + last + * return value on subsequent calls + */ + static Generator serialDayGenerator(final int interval, final DateValue dtStart) { + return new Generator() { + int year, month, date; + /** ndays in the last month encountered */ + int nDays; + + { + //step back one interval + DateValue dtStartMinus1; + { + DTBuilder builder = new DTBuilder(dtStart); + builder.day -= interval; + dtStartMinus1 = builder.toDate(); + } + year = dtStartMinus1.year(); + month = dtStartMinus1.month(); + date = dtStartMinus1.day(); + nDays = TimeUtils.monthLength(year, month); + } + + @Override + boolean generate(DTBuilder builder) { + int ndate; + if (year == builder.year && month == builder.month) { + ndate = date + interval; + if (ndate > nDays) { + return false; + } + } else { + nDays = TimeUtils.monthLength(builder.year, builder.month); + if (interval != 1) { + /* + * Calculate the number of days between the first of the + * new month and the old date and extend it to make it + * an integer multiple of interval. + */ + int daysBetween = TimeUtils.daysBetween(new DateValueImpl(builder.year, builder.month, 1), new DateValueImpl(year, month, date)); + ndate = ((interval - (daysBetween % interval)) % interval) + 1; + if (ndate > nDays) { + /* + * Need to return early without updating year or + * month so that the next time we enter with a + * different month, the daysBetween call above + * compares against the proper last date. + */ + return false; + } + } else { + ndate = 1; + } + year = builder.year; + month = builder.month; + } + date = builder.day = ndate; + return true; + } + + @Override + public String toString() { + return "serialDayGenerator:" + interval; + } + }; + } + + /** + * Constructs a generator that generates hours in the given date's day + * successively counting from the first hour passed in. + * @param interval number of hours to advance each step + * @param dtStart the start date + * @return the hour in dtStart the first time called and interval + last + * return value on subsequent calls + */ + static Generator serialHourGenerator(final int interval, final DateValue dtStart) { + final TimeValue dtStartTime = TimeUtils.timeOf(dtStart); + return new Generator() { + int hour = dtStartTime.hour() - interval; + int day = dtStart.day(); + int month = dtStart.month(); + int year = dtStart.year(); + + @Override + boolean generate(DTBuilder builder) { + int nhour; + if (day != builder.day || month != builder.month || year != builder.year) { + int hoursBetween = daysBetween(builder, year, month, day) * 24 - hour; + nhour = ((interval - (hoursBetween % interval)) % interval); + if (nhour > 23) { + /* + * Don't update day so that the difference calculation + * above is correct when this function is reentered with + * a different day. + */ + return false; + } + day = builder.day; + month = builder.month; + year = builder.year; + } else { + nhour = hour + interval; + if (nhour > 23) { + return false; + } + } + hour = builder.hour = nhour; + return true; + } + + @Override + public String toString() { + return "serialHourGenerator:" + interval; + } + }; + } + + /** + * Constructs a generator that generates minutes in the given date's hour + * successively counting from the first minute passed in. + * @param interval number of minutes to advance each step + * @param dtStart the date + * @return the minute in dtStart the first time called and interval + last + * return value on subsequent calls + */ + static Generator serialMinuteGenerator(final int interval, final DateValue dtStart) { + final TimeValue dtStartTime = TimeUtils.timeOf(dtStart); + return new Generator() { + int minute = dtStartTime.minute() - interval; + int hour = dtStartTime.hour(); + int day = dtStart.day(); + int month = dtStart.month(); + int year = dtStart.year(); + + @Override + boolean generate(DTBuilder builder) { + int nminute; + if (hour != builder.hour || day != builder.day || month != builder.month || year != builder.year) { + int minutesBetween = (daysBetween(builder, year, month, day) * 24 + builder.hour - hour) * 60 - minute; + nminute = ((interval - (minutesBetween % interval)) % interval); + if (nminute > 59) { + /* + * Don't update day so that the difference calculation + * above is correct when this function is reentered with + * a different day. + */ + return false; + } + hour = builder.hour; + day = builder.day; + month = builder.month; + year = builder.year; + } else { + nminute = minute + interval; + if (nminute > 59) { + return false; + } + } + minute = builder.minute = nminute; + return true; + } + + @Override + public String toString() { + return "serialMinuteGenerator:" + interval; + } + }; + } + + /** + * Constructs a generator that generates seconds in the given date's minute + * successively counting from the first second passed in. + * @param interval number of seconds to advance each step + * @param dtStart the date + * @return the second in dtStart the first time called and interval + last + * return value on subsequent calls + */ + static Generator serialSecondGenerator(final int interval, final DateValue dtStart) { + final TimeValue dtStartTime = TimeUtils.timeOf(dtStart); + return new Generator() { + int second = dtStartTime.second() - interval; + int minute = dtStartTime.minute(); + int hour = dtStartTime.hour(); + int day = dtStart.day(); + int month = dtStart.month(); + int year = dtStart.year(); + + @Override + boolean generate(DTBuilder builder) { + int nsecond; + if (minute != builder.minute || hour != builder.hour || day != builder.day || month != builder.month || year != builder.year) { + int secondsBetween = ((daysBetween(builder, year, month, day) * 24 + builder.hour - hour) * 60 + builder.minute - minute) * 60 - second; + nsecond = ((interval - (secondsBetween % interval)) % interval); + if (nsecond > 59) { + /* + * Don't update day so that the difference calculation + * above is correct when this function is reentered with + * a different day. + */ + return false; + } + minute = builder.minute; + hour = builder.hour; + day = builder.day; + month = builder.month; + year = builder.year; + } else { + nsecond = second + interval; + if (nsecond > 59) { + return false; + } + } + second = builder.second = nsecond; + return true; + } + + @Override + public String toString() { + return "serialSecondGenerator:" + interval; + } + }; + } + + /** + * Constructs a generator that yields the specified years in increasing + * order. + * @param years the years + * @param dtStart the start date + * @return the generator + */ + static Generator byYearGenerator(int[] years, final DateValue dtStart) { + final int[] uyears = Util.uniquify(years); + + // index into years + return new Generator() { + int i; + { + while (i < uyears.length && dtStart.year() > uyears[i]) { + ++i; + } + } + + @Override + boolean generate(DTBuilder builder) { + if (i >= uyears.length) { + return false; + } + builder.year = uyears[i++]; + return true; + } + + @Override + public String toString() { + return "byYearGenerator"; + } + }; + } + + /** + * Constructs a generator that yields the specified months in increasing + * order for each year. + * @param months the month values (each value must be in range [1,12]) + * @param dtStart the start date + * @return the generator + */ + static Generator byMonthGenerator(int[] months, final DateValue dtStart) { + final int[] umonths = Util.uniquify(months); + + return new Generator() { + int i; + int year = dtStart.year(); + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year) { + i = 0; + year = builder.year; + } + if (i >= umonths.length) { + return false; + } + builder.month = umonths[i++]; + return true; + } + + @Override + public String toString() { + return "byMonthGenerator:" + Arrays.toString(umonths); + } + }; + } + + /** + * Constructs a generator that yields the specified hours in increasing + * order for each day. + * @param hours the hour values (each value must be in range [0,23]) + * @param dtStart the start date + * @return the generator + */ + static Generator byHourGenerator(int[] hours, final DateValue dtStart) { + final TimeValue dtStartTime = TimeUtils.timeOf(dtStart); + final int[] uhours = (hours.length == 0) ? new int[] { dtStartTime.hour() } : Util.uniquify(hours); + + if (uhours.length == 1) { + final int hour = uhours[0]; + + return new SingleValueGenerator() { + int year; + int month; + int day; + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month || day != builder.day) { + year = builder.year; + month = builder.month; + day = builder.day; + builder.hour = hour; + return true; + } + return false; + } + + @Override + int getValue() { + return hour; + } + + @Override + public String toString() { + return "byHourGenerator:" + hour; + } + }; + } + + return new Generator() { + int i; + int year = dtStart.year(); + int month = dtStart.month(); + int day = dtStart.day(); + { + int hour = dtStartTime.hour(); + while (i < uhours.length && uhours[i] < hour) { + ++i; + } + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month || day != builder.day) { + i = 0; + year = builder.year; + month = builder.month; + day = builder.day; + } + if (i >= uhours.length) { + return false; + } + builder.hour = uhours[i++]; + return true; + } + + @Override + public String toString() { + return "byHourGenerator:" + Arrays.toString(uhours); + } + }; + } + + /** + * Constructs a generator that yields the specified minutes in increasing + * order for each hour. + * @param minutes the minute values (each value must be in range [0,59]) + * @param dtStart the start date + * @return the generator + */ + static Generator byMinuteGenerator(int[] minutes, final DateValue dtStart) { + final TimeValue dtStartTime = TimeUtils.timeOf(dtStart); + final int[] uminutes = (minutes.length == 0) ? new int[] { dtStartTime.minute() } : Util.uniquify(minutes); + + if (uminutes.length == 1) { + final int minute = uminutes[0]; + + return new SingleValueGenerator() { + int year; + int month; + int day; + int hour; + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month || day != builder.day || hour != builder.hour) { + year = builder.year; + month = builder.month; + day = builder.day; + hour = builder.hour; + builder.minute = minute; + return true; + } + return false; + } + + @Override + int getValue() { + return minute; + } + + @Override + public String toString() { + return "byMinuteGenerator:" + minute; + } + }; + } + + return new Generator() { + int i; + int year = dtStart.year(); + int month = dtStart.month(); + int day = dtStart.day(); + int hour = dtStartTime.hour(); + { + int minute = dtStartTime.minute(); + while (i < uminutes.length && uminutes[i] < minute) { + ++i; + } + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month || day != builder.day || hour != builder.hour) { + i = 0; + year = builder.year; + month = builder.month; + day = builder.day; + hour = builder.hour; + } + if (i >= uminutes.length) { + return false; + } + builder.minute = uminutes[i++]; + return true; + } + + @Override + public String toString() { + return "byMinuteGenerator:" + Arrays.toString(uminutes); + } + }; + } + + /** + * Constructs a generator that yields the specified seconds in increasing + * order for each minute. + * @param seconds the second values (each value must be in range [0,59]) + * @param dtStart the start date + * @return the generator + */ + static Generator bySecondGenerator(int[] seconds, final DateValue dtStart) { + final TimeValue dtStartTime = TimeUtils.timeOf(dtStart); + final int[] useconds = (seconds.length == 0) ? new int[] { dtStartTime.second() } : Util.uniquify(seconds); + + if (useconds.length == 1) { + final int second = useconds[0]; + + return new SingleValueGenerator() { + int year; + int month; + int day; + int hour; + int minute; + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month || day != builder.day || hour != builder.hour || minute != builder.minute) { + year = builder.year; + month = builder.month; + day = builder.day; + hour = builder.hour; + minute = builder.minute; + builder.second = second; + return true; + } + return false; + } + + @Override + int getValue() { + return second; + } + + @Override + public String toString() { + return "bySecondGenerator:" + second; + } + }; + } + + return new Generator() { + int i; + int year = dtStart.year(); + int month = dtStart.month(); + int day = dtStart.day(); + int hour = dtStartTime.hour(); + int minute = dtStartTime.minute(); + { + int second = dtStartTime.second(); + while (i < useconds.length && useconds[i] < second) { + ++i; + } + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month || day != builder.day || hour != builder.hour || minute != builder.minute) { + i = 0; + year = builder.year; + month = builder.month; + day = builder.day; + hour = builder.hour; + minute = builder.minute; + } + if (i >= useconds.length) { + return false; + } + builder.second = useconds[i++]; + return true; + } + + @Override + public String toString() { + return "bySecondGenerator:" + Arrays.toString(useconds); + } + }; + } + + /** + * Constructs a generator that yields the specified dates (possibly relative + * to end of month) in increasing order for each month seen. + * @param dates the date values (each value must be range [-31,31]) + * @param dtStart the start date + * @return the generator + */ + static Generator byMonthDayGenerator(int[] dates, final DateValue dtStart) { + final int[] udates = Util.uniquify(dates); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + /** list of generated dates for the current month */ + int[] posDates; + /** index of next date to return */ + int i = 0; + + { + convertDatesToAbsolute(); + } + + private void convertDatesToAbsolute() { + IntSet posDates = new IntSet(); + int nDays = TimeUtils.monthLength(year, month); + for (int j = 0; j < udates.length; ++j) { + int date = udates[j]; + if (date < 0) { + date += nDays + 1; + } + if (date >= 1 && date <= nDays) { + posDates.add(date); + } + } + this.posDates = posDates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month) { + year = builder.year; + month = builder.month; + + convertDatesToAbsolute(); + + i = 0; + } + if (i >= posDates.length) { + return false; + } + builder.day = posDates[i++]; + return true; + } + + @Override + public String toString() { + return "byMonthDayGenerator"; + } + }; + } + + /** + * Constructs a day generator based on a BYDAY rule. + * @param days list of week/number pairs (e.g. SU,3MO means every Sunday and + * the 3rd Monday) + * @param weeksInYear true if the week numbers are meant to be weeks in the + * current year, false if they are meant to be weeks in the current month + * @param dtStart the start date + * @return the generator + */ + static Generator byDayGenerator(ByDay[] days, final boolean weeksInYear, final DateValue dtStart) { + final ByDay[] udays = days.clone(); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + /** list of generated dates for the current month */ + int[] dates; + /** index of next date to return */ + int i = 0; + + { + generateDates(); + int day = dtStart.day(); + while (i < dates.length && dates[i] < day) { + ++i; + } + } + + void generateDates() { + int nDays; + DayOfWeek dow0; + int nDaysInMonth = TimeUtils.monthLength(year, month); + //index of the first day of the month in the month or year + int d0; + + if (weeksInYear) { + nDays = TimeUtils.yearLength(year); + dow0 = TimeUtils.firstDayOfWeekInMonth(year, 1); + d0 = TimeUtils.dayOfYear(year, month, 1); + } else { + nDays = nDaysInMonth; + dow0 = TimeUtils.firstDayOfWeekInMonth(year, month); + d0 = 0; + } + + /* + * An index not greater than the first week of the month in the + * month or year. + */ + int w0 = d0 / 7; + + /* + * Iterate through days and resolve each [week, day of week] + * pair to a day of the month. + */ + IntSet udates = new IntSet(); + for (ByDay day : udays) { + if (day.getNum() != null && day.getNum() != 0) { + int date = Util.dayNumToDate(dow0, nDays, day.getNum(), day.getDay(), d0, nDaysInMonth); + if (date != 0) { + udates.add(date); + } + } else { + int wn = w0 + 6; + for (int w = w0; w <= wn; ++w) { + int date = Util.dayNumToDate(dow0, nDays, w, day.getDay(), d0, nDaysInMonth); + if (date != 0) { + udates.add(date); + } + } + } + } + dates = udates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month) { + year = builder.year; + month = builder.month; + + generateDates(); + //start at the beginning of the month + i = 0; + } + if (i >= dates.length) { + return false; + } + builder.day = dates[i++]; + return true; + } + + @Override + public String toString() { + return "byDayGenerator:" + Arrays.toString(udays) + " by " + (weeksInYear ? "year" : "week"); + } + }; + } + + /** + * Constructs a generator that yields each day in the current month that + * falls in one of the given weeks of the year. + * @param weekNumbers the week numbers (each value must be in range + * [-53,53]) + * @param weekStart the day of the week that the week starts on + * @param dtStart the start date + * @return the generator + */ + static Generator byWeekNoGenerator(int[] weekNumbers, final DayOfWeek weekStart, final DateValue dtStart) { + final int[] uWeekNumbers = Util.uniquify(weekNumbers); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + /** number of weeks in the last year seen */ + int weeksInYear; + /** dates generated anew for each month seen */ + int[] dates; + /** index into dates */ + int i = 0; + + /** + * day of the year of the start of week 1 of the current year. Since + * week 1 may start on the previous year, this may be negative. + */ + int doyOfStartOfWeek1; + + { + checkYear(); + checkMonth(); + } + + void checkYear() { + //if the first day of January is weekStart, then there are 7 + //if the first day of January is weekStart + 1, then there are 6 + //if the first day of January is weekStart + 6, then there is 1 + DayOfWeek dowJan1 = TimeUtils.firstDayOfWeekInMonth(year, 1); + int nDaysInFirstWeek = 7 - ((7 + dowJan1.getCalendarConstant() - weekStart.getCalendarConstant()) % 7); + + //number of days not in any week + int nOrphanedDays = 0; + + /* + * According to RFC 2445: + * + * Week number one of the calendar year is the first week which + * contains at least four (4) days in that calendar year. + */ + if (nDaysInFirstWeek < 4) { + nOrphanedDays = nDaysInFirstWeek; + nDaysInFirstWeek = 7; + } + + /* + * Calculate the day of year (possibly negative) of the start of + * the first week in the year. This day must be of weekStart. + */ + doyOfStartOfWeek1 = nDaysInFirstWeek - 7 + nOrphanedDays; + + weeksInYear = (TimeUtils.yearLength(year) - nOrphanedDays + 6) / 7; + } + + void checkMonth() { + //the day of the year of the 1st day in the month + int doyOfMonth1 = TimeUtils.dayOfYear(year, month, 1); + + //the week of the year of the 1st day of the month. approximate. + int weekOfMonth = ((doyOfMonth1 - doyOfStartOfWeek1) / 7) + 1; + + //the number of days in the month + int nDays = TimeUtils.monthLength(year, month); + + //generate the dates in the month + IntSet udates = new IntSet(); + for (int weekNo : uWeekNumbers) { + if (weekNo < 0) { + weekNo += weeksInYear + 1; + } + if (weekNo >= weekOfMonth - 1 && weekNo <= weekOfMonth + 6) { + for (int d = 0; d < 7; ++d) { + int date = ((weekNo - 1) * 7 + d + doyOfStartOfWeek1 - doyOfMonth1) + 1; + if (date >= 1 && date <= nDays) { + udates.add(date); + } + } + } + } + dates = udates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + /* + * This is a bit odd, since we're generating days within the + * given weeks of the year within the month/year from builder. + */ + if (year != builder.year || month != builder.month) { + if (year != builder.year) { + year = builder.year; + checkYear(); + } + month = builder.month; + checkMonth(); + + i = 0; + } + + if (i >= dates.length) { + return false; + } + builder.day = dates[i++]; + return true; + } + + @Override + public String toString() { + return "byWeekNoGenerator"; + } + }; + } + + /** + * Constructs a day generator that generates dates in the current month that + * fall on one of the given days of the year. + * @param yearDays the days of the year (values must be in range [-366,366]) + * @param dtStart the start date + * @return the generator + */ + static Generator byYearDayGenerator(int[] yearDays, final DateValue dtStart) { + final int[] uYearDays = Util.uniquify(yearDays); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + int[] dates; + int i = 0; + + { + checkMonth(); + } + + void checkMonth() { + //now, calculate the first week of the month + int doyOfMonth1 = TimeUtils.dayOfYear(year, month, 1); + int nDays = TimeUtils.monthLength(year, month); + int nYearDays = TimeUtils.yearLength(year); + IntSet udates = new IntSet(); + for (int yearDay : uYearDays) { + if (yearDay < 0) { + yearDay += nYearDays + 1; + } + int date = yearDay - doyOfMonth1; + if (date >= 1 && date <= nDays) { + udates.add(date); + } + } + dates = udates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month) { + year = builder.year; + month = builder.month; + + checkMonth(); + + i = 0; + } + if (i >= dates.length) { + return false; + } + builder.day = dates[i++]; + return true; + } + + @Override + public String toString() { + return "byYearDayGenerator"; + } + }; + } + + private static int daysBetween(DTBuilder builder, int year, int month, int day) { + if (year == builder.year && month == builder.month) { + return builder.day - day; + } else { + return TimeUtils.daysBetween(builder.year, builder.month, builder.day, year, month, day); + } + } + + private Generators() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/InstanceGenerators.java b/app/src/main/java/biweekly/util/com/google/ical/iter/InstanceGenerators.java new file mode 100644 index 0000000000..1cfc90a8e0 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/InstanceGenerators.java @@ -0,0 +1,356 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.ArrayList; +import java.util.List; + +import biweekly.util.DayOfWeek; +import biweekly.util.Frequency; +import biweekly.util.com.google.ical.util.DTBuilder; +import biweekly.util.com.google.ical.util.Predicate; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + * Factory for generators that operate on groups of generators to generate full + * dates. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +class InstanceGenerators { + /** + * A collector that yields each date in the period without doing any set + * collecting. + */ + static Generator serialInstanceGenerator(final Predicate filter, final Generator yearGenerator, final Generator monthGenerator, final Generator dayGenerator, final Generator hourGenerator, final Generator minuteGenerator, final Generator secondGenerator) { + if (skipSubDayGenerators(hourGenerator, minuteGenerator, secondGenerator)) { + //fast case for generators that are not more frequent than daily + return new Generator() { + @Override + public boolean generate(DTBuilder builder) throws IteratorShortCircuitingException { + //cascade through periods to compute the next date + do { + //until we run out of days in the current month + while (!dayGenerator.generate(builder)) { + //until we run out of months in the current year + while (!monthGenerator.generate(builder)) { + //if there are more years available fetch one + if (!yearGenerator.generate(builder)) { + //otherwise the recurrence is exhausted + return false; + } + } + } + //apply filters to generated dates + } while (!filter.apply(builder.toDateTime())); + + return true; + } + }; + } else { + return new Generator() { + @Override + public boolean generate(DTBuilder builder) throws IteratorShortCircuitingException { + //cascade through periods to compute the next date + do { + //until we run out of seconds in the current minute + while (!secondGenerator.generate(builder)) { + //until we run out of minutes in the current hour + while (!minuteGenerator.generate(builder)) { + //until we run out of hours in the current day + while (!hourGenerator.generate(builder)) { + //until we run out of days in the current month + while (!dayGenerator.generate(builder)) { + //until we run out of months in the current year + while (!monthGenerator.generate(builder)) { + //if there are more years available fetch one + if (!yearGenerator.generate(builder)) { + //otherwise the recurrence is exhausted + return false; + } + } + } + } + } + } + //apply filters to generated dates + } while (!filter.apply(builder.toDateTime())); + //TODO: maybe group the filters into different kinds so we don't + //apply filters that only affect days to every second. + + return true; + } + }; + } + } + + static Generator bySetPosInstanceGenerator(int[] setPos, final Frequency freq, final DayOfWeek wkst, final Predicate filter, final Generator yearGenerator, final Generator monthGenerator, final Generator dayGenerator, final Generator hourGenerator, final Generator minuteGenerator, final Generator secondGenerator) { + final int[] uSetPos = Util.uniquify(setPos); + + final Generator serialInstanceGenerator = serialInstanceGenerator(filter, yearGenerator, monthGenerator, dayGenerator, hourGenerator, minuteGenerator, secondGenerator); + + //TODO(msamuel): does this work? + final int maxPos = uSetPos[uSetPos.length - 1]; + final boolean allPositive = uSetPos[0] > 0; + + return new Generator() { + DateValue pushback = null; + + /** + * Is this the first instance we generate? We need to know so that + * we don't clobber dtStart. + */ + boolean first = true; + + /** + * Do we need to halt iteration once the current set has been used? + */ + boolean done = false; + + /** + * The elements in the current set, filtered by set pos. + */ + List candidates; + + /** + * Index into candidates. The number of elements in candidates + * already consumed. + */ + int i; + + @Override + public boolean generate(DTBuilder builder) throws IteratorShortCircuitingException { + while (candidates == null || i >= candidates.size()) { + if (done) { + return false; + } + + /* + * (1) Make sure that builder is appropriately initialized + * so that we only generate instances in the next set. + */ + DateValue d0 = null; + if (pushback != null) { + d0 = pushback; + builder.year = d0.year(); + builder.month = d0.month(); + builder.day = d0.day(); + pushback = null; + } else if (!first) { + /* + * We need to skip ahead to the next item since we + * didn't exhaust the last period. + */ + switch (freq) { + case YEARLY: + if (!yearGenerator.generate(builder)) { + return false; + } + // $FALL-THROUGH$ + case MONTHLY: + while (!monthGenerator.generate(builder)) { + if (!yearGenerator.generate(builder)) { + return false; + } + } + break; + case WEEKLY: + //consume because just incrementing date doesn't do anything + DateValue nextWeek = Util.nextWeekStart(builder.toDateTime(), wkst); + do { + if (!serialInstanceGenerator.generate(builder)) { + return false; + } + } while (builder.compareTo(nextWeek) < 0); + d0 = builder.toDateTime(); + break; + default: + break; + } + } else { + first = false; + } + + /* + * (2) Build a set of the dates in the year/month/week that + * match the other rule. + */ + List dates = new ArrayList(); + if (d0 != null) { + dates.add(d0); + } + + /* + * Optimization: if min(bySetPos) > 0 then we already have + * absolute positions, so we don't need to generate all of + * the instances for the period. This speeds up things like + * the first weekday of the year: + * + * RRULE:FREQ=YEARLY;BYDAY=MO,TU,WE,TH,FR,BYSETPOS=1 + * + * That would otherwise generate 260+ instances per one + * emitted. + * + * TODO(msamuel): this may be premature. If needed, We could + * improve more generally by inferring a BYMONTH generator + * based on distribution of set positions within the year. + */ + int limit = allPositive ? maxPos : Integer.MAX_VALUE; + + while (limit > dates.size()) { + if (!serialInstanceGenerator.generate(builder)) { + /* + * If we can't generate any, then make sure we + * return false once the instances we have generated + * are exhausted. If this is returning false due to + * some artificial limit, such as the 100 year limit + * in serialYearGenerator, then we exit via an + * exception because otherwise we would pick the + * wrong elements for some uSetPoses that contain + * negative elements. + */ + done = true; + break; + } + DateValue d = builder.toDateTime(); + boolean contained; + if (d0 == null) { + d0 = d; + contained = true; + } else { + switch (freq) { + case WEEKLY: + int nb = TimeUtils.daysBetween(d, d0); + /* + * Two dates (d, d0) are in the same week if + * there isn't a whole week in between them and + * the later day is later in the week than the + * earlier day. + */ + //@formatter:off + contained = + nb < 7 && + ((7 + TimeUtils.dayOfWeek(d).getCalendarConstant() + - wkst.getCalendarConstant()) % 7) > + ((7 + TimeUtils.dayOfWeek(d0).getCalendarConstant() + - wkst.getCalendarConstant()) % 7); + //@formatter:on + break; + case MONTHLY: + contained = d0.month() == d.month() && d0.year() == d.year(); + break; + case YEARLY: + contained = d0.year() == d.year(); + break; + default: + done = true; + return false; + } + } + if (contained) { + dates.add(d); + } else { + //reached end of the set + pushback = d; //save d so we can use it later + break; + } + } + + /* + * (3) Resolve the positions to absolute positions and order + * them. + */ + int[] absSetPos; + if (allPositive) { + absSetPos = uSetPos; + } else { + IntSet uAbsSetPos = new IntSet(); + for (int p : uSetPos) { + if (p < 0) { + p = dates.size() + p + 1; + } + uAbsSetPos.add(p); + } + absSetPos = uAbsSetPos.toIntArray(); + } + + candidates = new ArrayList(); + for (int p : absSetPos) { + if (p >= 1 && p <= dates.size()) { // p is 1-indexed + candidates.add(dates.get(p - 1)); + } + } + i = 0; + if (candidates.isEmpty()) { + //none in this region, so keep looking + candidates = null; + continue; + } + } + + /* + * (5) Emit a date. It will be checked against the end condition + * and dtStart elsewhere. + */ + DateValue d = candidates.get(i++); + builder.year = d.year(); + builder.month = d.month(); + builder.day = d.day(); + if (d instanceof TimeValue) { + TimeValue t = (TimeValue) d; + builder.hour = t.hour(); + builder.minute = t.minute(); + builder.second = t.second(); + } + return true; + } + }; + } + + static boolean skipSubDayGenerators(Generator hourGenerator, Generator minuteGenerator, Generator secondGenerator) { + return secondGenerator instanceof SingleValueGenerator && minuteGenerator instanceof SingleValueGenerator && hourGenerator instanceof SingleValueGenerator; + } + + private InstanceGenerators() { + // uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/IntSet.java b/app/src/main/java/biweekly/util/com/google/ical/iter/IntSet.java new file mode 100644 index 0000000000..e3759b14d6 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/IntSet.java @@ -0,0 +1,133 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.BitSet; + +/** + * A set of integers in a small range. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class IntSet { + private final BitSet ints = new BitSet(); + + /** + * Adds an integer to the set. + * @param n the integer to add + */ + void add(int n) { + ints.set(encode(n)); + } + + /** + * Determines if an integer is contained within the set. + * @param n the integer to look for + * @return true if it's in the set, false if not + */ + boolean contains(int n) { + return ints.get(encode(n)); + } + + /** + * Gets the number of integers in the set. + * @return the number of integers + */ + int size() { + return ints.cardinality(); + } + + /** + * Converts this set to a new integer array that is sorted in ascending + * order. + * @return the array (sorted in ascending order) + */ + int[] toIntArray() { + int[] out = new int[size()]; + int a = 0, b = out.length; + for (int i = -1; (i = ints.nextSetBit(i + 1)) >= 0;) { + int n = decode(i); + if (n < 0) { + out[a++] = n; + } else { + out[--b] = n; + } + } + + //if it contains -3, -1, 0, 1, 2, 4 + //then out will be -1, -3, 4, 2, 1, 0 + reverse(out, 0, a); + reverse(out, a, out.length); + + return out; + } + + /** + * Encodes an integer so it can be inserted into the set. + * @param n the integer to insert + * @return the encoded value to insert into the set + */ + private static int encode(int n) { + return n < 0 ? ((-n << 1) + 1) : (n << 1); + } + + /** + * Decodes an integer from the set. + * @param i the integer to decode + * @return the decoded integer + */ + private static int decode(int i) { + return (i >>> 1) * (-(i & 1) | 1); + } + + /** + * Reverses an array. + * @param array the array + * @param start the index to start at + * @param end the index to end at + */ + private static void reverse(int[] array, int start, int end) { + for (int i = start, j = end; i < --j; ++i) { + int t = array[i]; + array[i] = array[j]; + array[j] = t; + } + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/RDateIteratorImpl.java b/app/src/main/java/biweekly/util/com/google/ical/iter/RDateIteratorImpl.java new file mode 100644 index 0000000000..30523db6f7 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/RDateIteratorImpl.java @@ -0,0 +1,109 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +import biweekly.util.com.google.ical.values.DateValue; + +/** + * A recurrence iterator that iterates over an array of dates. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class RDateIteratorImpl implements RecurrenceIterator { + private final DateValue[] datesUtc; + private int i; + + /** + * Creates a new recurrence iterator. + * @param datesUtc the dates to iterate over (assumes they are all in UTC) + */ + RDateIteratorImpl(DateValue[] datesUtc) { + datesUtc = datesUtc.clone(); + Arrays.sort(datesUtc); + this.datesUtc = removeDuplicates(datesUtc); + } + + public boolean hasNext() { + return i < datesUtc.length; + } + + public DateValue next() { + if (i >= datesUtc.length) { + throw new NoSuchElementException(); + } + return datesUtc[i++]; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void advanceTo(DateValue newStartUtc) { + long startCmp = DateValueComparison.comparable(newStartUtc); + while (i < datesUtc.length && startCmp > DateValueComparison.comparable(datesUtc[i])) { + ++i; + } + } + + /** + * Removes duplicates from a list of date values. + * @param dates the date values (must be sorted in ascending order) + * @return a new array if any elements were removed or the original array if + * no elements were removed + */ + private static DateValue[] removeDuplicates(DateValue[] dates) { + int k = 0; + for (int i = 1; i < dates.length; ++i) { + if (!dates[i].equals(dates[k])) { + dates[++k] = dates[i]; + } + } + + if (++k < dates.length) { + DateValue[] uniqueDates = new DateValue[k]; + System.arraycopy(dates, 0, uniqueDates, 0, k); + return uniqueDates; + } + return dates; + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/RRuleIteratorImpl.java b/app/src/main/java/biweekly/util/com/google/ical/iter/RRuleIteratorImpl.java new file mode 100644 index 0000000000..6387061311 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/RRuleIteratorImpl.java @@ -0,0 +1,354 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.TimeZone; + +import biweekly.util.com.google.ical.util.DTBuilder; +import biweekly.util.com.google.ical.util.Predicate; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + * Iterates over dates in an RRULE or EXRULE series. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +final class RRuleIteratorImpl implements RecurrenceIterator { + /** + * Determines when the recurrence ends. The condition is applied + * after the date is converted to UTC. + */ + private final Predicate condition; + + /** + * Applies the various period generators to generate an entire date. This + * may involve generating a set of dates and discarding all but those that + * match the BYSETPOS rule. + */ + private final Generator instanceGenerator; + + /** + * Populates the builder's year field. Returns false if there aren't more + * years available. + */ + private final ThrottledGenerator yearGenerator; + + /** + * Populates the builder's month field. Returns false if there aren't more + * months available in the builder's year. + */ + private final Generator monthGenerator; + + /** + * A date that has been computed but not yet yielded to the user. + */ + private DateValue pendingUtc; + + /** + * Used to build successive dates. At the start of the building process, + * this contains the last date generated. Different periods are successively + * inserted into it. + */ + private DTBuilder builder; + + /** + * True iff the recurrence has been exhausted. + */ + private boolean done; + + /** + * The start date of the recurrence. + */ + private final DateValue dtStart; + + /** + * False iff shortcutting advance would break the semantics of the + * iteration. This may happen when, for example, the end condition requires + * that it see every item. + */ + private final boolean canShortcutAdvance; + + /** + * The timezone that resultant dates should be converted from. All + * date fields, parameters, and local variables in this class are in this + * timezone, unless they carry the UTC suffix. + */ + private final TimeZone tzid; + + /** + * Creates the iterator. + * @param dtStart the start date of the recurrence + * @param tzid the timezone that resultant dates should be converted from + * @param condition determines when the recurrence ends + * @param instanceGenerator applies the various period generators to + * generate an entire date + * @param yearGenerator populates each date's year field + * @param monthGenerator populates each date's month field + * @param dayGenerator populates each date's day field + * @param hourGenerator populates each date's hour field + * @param minuteGenerator populates each date's minute field + * @param secondGenerator populates each date's second field + * @param canShortcutAdvance false iff shortcutting advance would break the + * semantics of the iteration, true if not + */ + RRuleIteratorImpl(DateValue dtStart, TimeZone tzid, Predicate condition, Generator instanceGenerator, ThrottledGenerator yearGenerator, Generator monthGenerator, Generator dayGenerator, Generator hourGenerator, Generator minuteGenerator, Generator secondGenerator, boolean canShortcutAdvance) { + + this.condition = condition; + this.instanceGenerator = instanceGenerator; + this.yearGenerator = yearGenerator; + this.monthGenerator = monthGenerator; + this.dtStart = dtStart; + this.tzid = tzid; + this.canShortcutAdvance = canShortcutAdvance; + + int initWorkLimit = 1000; + + /* + * Initialize the builder and skip over any extraneous start instances. + */ + builder = new DTBuilder(dtStart); + + /* + * Apply the generators from largest field to smallest so we can start + * by applying the smallest field iterator when asked to generate a + * date. + */ + try { + Generator[] toInitialize; + if (InstanceGenerators.skipSubDayGenerators(hourGenerator, minuteGenerator, secondGenerator)) { + toInitialize = new Generator[] { yearGenerator, monthGenerator }; + builder.hour = ((SingleValueGenerator) hourGenerator).getValue(); + builder.minute = ((SingleValueGenerator) minuteGenerator).getValue(); + builder.second = ((SingleValueGenerator) secondGenerator).getValue(); + } else { + toInitialize = new Generator[] { yearGenerator, monthGenerator, dayGenerator, hourGenerator, minuteGenerator, }; + } + for (int i = 0; i != toInitialize.length;) { + if (toInitialize[i].generate(builder)) { + ++i; + } else { + if (--i < 0) { //no years left + done = true; + break; + } + } + + if (--initWorkLimit == 0) { + done = true; + break; + } + } + } catch (Generator.IteratorShortCircuitingException ex) { + done = true; + } + + while (!done) { + pendingUtc = generateInstance(); + if (pendingUtc == null) { + done = true; + break; + } + + if (pendingUtc.compareTo(TimeUtils.toUtc(dtStart, tzid)) >= 0) { + /* + * We only apply the condition to the ones past dtStart to avoid + * counting useless instances. + */ + if (!condition.apply(pendingUtc)) { + done = true; + pendingUtc = null; + } + break; + } + + if (--initWorkLimit == 0) { + done = true; + break; + } + } + } + + public boolean hasNext() { + if (pendingUtc == null) { + fetchNext(); + } + return pendingUtc != null; + } + + public DateValue next() { + if (pendingUtc == null) { + fetchNext(); + } + DateValue next = pendingUtc; + pendingUtc = null; + return next; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void advanceTo(DateValue dateUtc) { + /* + * Don't throw away a future pending date since the iterators will not + * generate it again. + */ + if (pendingUtc != null && dateUtc.compareTo(pendingUtc) <= 0) { + return; + } + + DateValue dateLocal = TimeUtils.fromUtc(dateUtc, tzid); + + //short-circuit if we're already past dateUtc + if (dateLocal.compareTo(builder.toDate()) <= 0) { + return; + } + + pendingUtc = null; + + try { + if (canShortcutAdvance) { + //skip years before date.year + if (builder.year < dateLocal.year()) { + do { + if (!yearGenerator.generate(builder)) { + done = true; + return; + } + } while (builder.year < dateLocal.year()); + while (!monthGenerator.generate(builder)) { + if (!yearGenerator.generate(builder)) { + done = true; + return; + } + } + } + + if (builder.month < dateLocal.month()) { + builder.day = 1; + } + + //skip months before date.year/date.month + while (builder.year == dateLocal.year() && builder.month < dateLocal.month()) { + while (!monthGenerator.generate(builder)) { + //if there are more years available fetch one + if (!yearGenerator.generate(builder)) { + //otherwise the recurrence is exhausted + done = true; + return; + } + } + } + } + + //consume any remaining instances + while (!done) { + DateValue dUtc = generateInstance(); + if (dUtc == null) { + done = true; + return; + } + + if (!condition.apply(dUtc)) { + done = true; + return; + } + + if (dUtc.compareTo(dateUtc) >= 0) { + pendingUtc = dUtc; + break; + } + } + } catch (Generator.IteratorShortCircuitingException ex) { + done = true; + } + } + + /** calculates and stored the next date in this recurrence. */ + private void fetchNext() { + if (pendingUtc != null || done) { + return; + } + + DateValue dUtc = generateInstance(); + + //check the exit condition + if (dUtc == null || !condition.apply(dUtc)) { + done = true; + return; + } + + pendingUtc = dUtc; + yearGenerator.workDone(); + } + + private static final DateValue MIN_DATE = new DateValueImpl(Integer.MIN_VALUE, 1, 1); + + /** + * Make sure the iterator is monotonically increasing. The local time is + * guaranteed to be monotonic, but because of daylight savings shifts, the + * time in UTC may not be. + */ + private DateValue lastUtc_ = MIN_DATE; + + /** + * Generates a date. + * @return a date value in UTC or null if a date value could not be + * generated + */ + private DateValue generateInstance() { + try { + do { + if (!instanceGenerator.generate(builder)) { + return null; + } + DateValue dUtc = dtStart instanceof TimeValue ? TimeUtils.toUtc(builder.toDateTime(), tzid) : builder.toDate(); + if (dUtc.compareTo(lastUtc_) > 0) { + return dUtc; + } + } while (true); + } catch (Generator.IteratorShortCircuitingException ex) { + return null; + } + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIterable.java b/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIterable.java new file mode 100644 index 0000000000..b42dfa6bc1 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIterable.java @@ -0,0 +1,51 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import biweekly.util.com.google.ical.values.DateValue; + +/** + * Iterates over a series of dates in a recurrence rule in ascending order. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public interface RecurrenceIterable extends Iterable { + RecurrenceIterator iterator(); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIterator.java b/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIterator.java new file mode 100644 index 0000000000..7b92f10de2 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIterator.java @@ -0,0 +1,79 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.Iterator; + +import biweekly.util.com.google.ical.values.DateValue; + +/** + * Iterates over a series of dates in a recurrence rule in ascending order. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public interface RecurrenceIterator extends Iterator { + /** + * Determines if there are more dates in the series. + * @return true if there are more dates, false if not + */ + boolean hasNext(); + + /** + * Returns the next date in the series. If {@link #hasNext()} returns + * {@code false}, then this method's behavior is undefined. + * @return the next date (in UTC; will be strictly later than any date + * previously returned by this iterator) + */ + DateValue next(); + + /** + * Skips all dates in the series that come before the given date, so that + * the next call to {@link #next} will return a date on or after the given + * date (assuming the recurrence includes such a date). + * @param newStartUtc the date to advance to (in UTC) + */ + void advanceTo(DateValue newStartUtc); + + /** + * Implementors of this interface are not expected to implement this method. + * @throws UnsupportedOperationException always + */ + void remove(); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIteratorFactory.java b/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIteratorFactory.java new file mode 100644 index 0000000000..d13c09739c --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/RecurrenceIteratorFactory.java @@ -0,0 +1,569 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.TimeZone; + +import biweekly.util.ByDay; +import biweekly.util.DayOfWeek; +import biweekly.util.Frequency; +import biweekly.util.Google2445Utils; +import biweekly.util.ICalDate; +import biweekly.util.Recurrence; +import biweekly.util.com.google.ical.util.Predicate; +import biweekly.util.com.google.ical.util.Predicates; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateTimeValue; +import biweekly.util.com.google.ical.values.DateTimeValueImpl; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + *

+ * Calculates the occurrences of an individual RRULE definition or groups of + * RRULEs, RDATEs, EXRULEs, and EXDATEs. + *

+ *

+ * Glossary + *

+ *
    + *
  • Period - year|month|day|...
  • + *
  • Day of the week - an int in [0,6]
  • + *
  • Day of the year - zero indexed in [0,365]
  • + *
  • Day of the month - 1 indexed in [1,31]
  • + *
  • Month - 1 indexed integer in [1,12]
  • + *
+ *

+ * Abstractions + *

+ *
    + *
  • Generator - a function corresponding to an RRULE part that takes a date + * and returns a later (year or month or day depending on its period) within the + * next larger period. A generator ignores all periods in its input smaller than + * its period.
  • + *
  • Filter - a function that returns true iff the given date matches the + * subrule.
  • + *
  • Condition - returns true if the given date is past the end of the + * recurrence.
  • + *
+ *

+ * All the functions that represent rule parts are stateful. + *

+ * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public class RecurrenceIteratorFactory { + /** + * Creates a recurrence iterator from an RDATE or EXDATE list. + * @param dates the list of dates + * @return the iterator + */ + public static RecurrenceIterator createRecurrenceIterator(Collection dates) { + DateValue[] datesArray = dates.toArray(new DateValue[0]); + return new RDateIteratorImpl(datesArray); + } + + /** + * Creates a recurrence iterable from an RRULE. + * @param rrule the recurrence rule + * @param dtStart the start date of the series + * @param tzid the timezone that the start date is in, as well as the + * timezone to iterate in + * @return the iterable + */ + public static RecurrenceIterable createRecurrenceIterable(final Recurrence rrule, final DateValue dtStart, final TimeZone tzid) { + return new RecurrenceIterable() { + public RecurrenceIterator iterator() { + return createRecurrenceIterator(rrule, dtStart, tzid); + } + }; + } + + /** + * Creates a recurrence iterator from an RRULE. + * @param rrule the recurrence rule + * @param dtStart the start date of the series + * @param tzid the timezone that the start date is in, as well as the + * timezone to iterate in + * @return the iterator + */ + public static RecurrenceIterator createRecurrenceIterator(Recurrence rrule, DateValue dtStart, TimeZone tzid) { + Frequency freq = rrule.getFrequency(); + + /* + * If the given RRULE is malformed and does not have a frequency + * specified, default to "yearly". + */ + if (freq == null) { + freq = Frequency.YEARLY; + } + + DayOfWeek wkst = rrule.getWorkweekStarts(); + + ICalDate until = rrule.getUntil(); + int count = toInt(rrule.getCount()); + int interval = toInt(rrule.getInterval()); + ByDay[] byDay = rrule.getByDay().toArray(new ByDay[0]); + int[] byMonth = toIntArray(rrule.getByMonth()); + int[] byMonthDay = toIntArray(rrule.getByMonthDay()); + int[] byWeekNo = toIntArray(rrule.getByWeekNo()); + int[] byYearDay = toIntArray(rrule.getByYearDay()); + int[] bySetPos = toIntArray(rrule.getBySetPos()); + int[] byHour = toIntArray(rrule.getByHour()); + int[] byMinute = toIntArray(rrule.getByMinute()); + int[] bySecond = toIntArray(rrule.getBySecond()); + boolean canShortcutAdvance = true; + + if (interval <= 0) { + interval = 1; + } + + if (wkst == null) { + wkst = DayOfWeek.MONDAY; + } + + //optimize out BYSETPOS where possible + if (bySetPos.length > 0) { + switch (freq) { + case HOURLY: + if (byHour.length > 0 && byMinute.length <= 1 && bySecond.length <= 1) { + byHour = filterBySetPos(byHour, bySetPos); + } + + /* + * Handling bySetPos for rules that are more frequent than daily + * tends to lead to large amounts of processor being used before + * other work limiting features can kick in since there many + * seconds between dtStart and where the year limit kicks in. + * There are no known use cases for the use of bySetPos with + * hourly minutely and secondly rules so we just ignore it. + */ + bySetPos = NO_INTS; + break; + case MINUTELY: + if (byMinute.length > 0 && bySecond.length <= 1) { + byMinute = filterBySetPos(byMinute, bySetPos); + } + //see bySetPos handling comment above + bySetPos = NO_INTS; + break; + case SECONDLY: + if (bySecond.length > 0) { + bySecond = filterBySetPos(bySecond, bySetPos); + } + //see bySetPos handling comment above + bySetPos = NO_INTS; + break; + default: + } + + canShortcutAdvance = false; + } + + DateValue start = dtStart; + if (bySetPos.length > 0) { + /* + * Roll back until the beginning of the period to make sure that any + * positive indices are indexed properly. The actual iterator + * implementation is responsible for anything < dtStart. + */ + switch (freq) { + case YEARLY: + if (dtStart instanceof TimeValue) { + TimeValue tv = (TimeValue) dtStart; + start = new DateTimeValueImpl(start.year(), 1, 1, tv.hour(), tv.minute(), tv.second()); + } else { + start = new DateValueImpl(start.year(), 1, 1); + } + break; + case MONTHLY: + if (dtStart instanceof TimeValue) { + TimeValue tv = (TimeValue) dtStart; + start = new DateTimeValueImpl(start.year(), start.month(), 1, tv.hour(), tv.minute(), tv.second()); + } else { + start = new DateValueImpl(start.year(), start.month(), 1); + } + break; + case WEEKLY: + int d = (7 + wkst.ordinal() - TimeUtils.dayOfWeek(dtStart).getCalendarConstant()) % 7; + start = TimeUtils.add(dtStart, new DateValueImpl(0, 0, -d)); + break; + default: + break; + } + } + + /* + * Recurrences are implemented as a sequence of periodic generators. + * First a year is generated, and then months, and within months, days. + */ + ThrottledGenerator yearGenerator = Generators.serialYearGenerator(freq == Frequency.YEARLY ? interval : 1, dtStart); + Generator monthGenerator = null; + Generator dayGenerator = null; + Generator secondGenerator = null; + Generator minuteGenerator = null; + Generator hourGenerator = null; + + /* + * When multiple generators are specified for a period, they act as a + * union operator. We could have multiple generators (say, for day) and + * then run each and merge the results, but some generators are more + * efficient than others. So to avoid generating 53 Sundays and throwing + * away all but 1 for RRULE:FREQ=YEARLY;BYDAY=TU;BYWEEKNO=1, we + * reimplement some of the more prolific generators as filters. + */ + // TODO(msamuel): don't need a list here + List> filters = new ArrayList>(); + + switch (freq) { + case SECONDLY: + if (bySecond.length == 0 || interval != 1) { + secondGenerator = Generators.serialSecondGenerator(interval, dtStart); + if (bySecond.length > 0) { + filters.add(Filters.bySecondFilter(bySecond)); + } + } + break; + case MINUTELY: + if (byMinute.length == 0 || interval != 1) { + minuteGenerator = Generators.serialMinuteGenerator(interval, dtStart); + if (byMinute.length > 0) { + filters.add(Filters.byMinuteFilter(byMinute)); + } + } + break; + case HOURLY: + if (byHour.length == 0 || interval != 1) { + hourGenerator = Generators.serialHourGenerator(interval, dtStart); + if (byHour.length > 0) { + filters.add(Filters.byHourFilter(bySecond)); + } + } + break; + case DAILY: + break; + case WEEKLY: + /* + * Week is not considered a period because a week may span multiple + * months and/or years. There are no week generators, so a filter is + * used to make sure that FREQ=WEEKLY;INTERVAL=2 only generates + * dates within the proper week. + */ + if (byDay.length > 0) { + dayGenerator = Generators.byDayGenerator(byDay, false, start); + byDay = NO_DAYS; + if (interval > 1) { + filters.add(Filters.weekIntervalFilter(interval, wkst, dtStart)); + } + } else { + dayGenerator = Generators.serialDayGenerator(interval * 7, dtStart); + } + break; + case YEARLY: + if (byYearDay.length > 0) { + /* + * The BYYEARDAY rule part specifies a COMMA separated list of + * days of the year. Valid values are 1 to 366 or -366 to -1. + * For example, -1 represents the last day of the year (December + * 31st) and -306 represents the 306th to the last day of the + * year (March 1st). + */ + dayGenerator = Generators.byYearDayGenerator(byYearDay, start); + break; + } + // $FALL-THROUGH$ + case MONTHLY: + if (byMonthDay.length > 0) { + /* + * The BYMONTHDAY rule part specifies a COMMA separated list of + * days of the month. Valid values are 1 to 31 or -31 to -1. For + * example, -10 represents the tenth to the last day of the + * month. + */ + dayGenerator = Generators.byMonthDayGenerator(byMonthDay, start); + byMonthDay = NO_INTS; + } else if (byWeekNo.length > 0 && Frequency.YEARLY == freq) { + /* + * The BYWEEKNO rule part specifies a COMMA separated list of + * ordinals specifying weeks of the year. This rule part is only + * valid for YEARLY rules. + */ + dayGenerator = Generators.byWeekNoGenerator(byWeekNo, wkst, start); + byWeekNo = NO_INTS; + } else if (byDay.length > 0) { + /* + * Each BYDAY value can also be preceded by a positive (n) or + * negative (-n) integer. If present, this indicates the nth + * occurrence of the specific day within the MONTHLY or YEARLY + * RRULE. For example, within a MONTHLY rule, +1MO (or simply + * 1MO) represents the first Monday within the month, whereas + * -1MO represents the last Monday of the month. If an integer + * modifier is not present, it means all days of this type + * within the specified frequency. For example, within a MONTHLY + * rule, MO represents all Mondays within the month. + */ + dayGenerator = Generators.byDayGenerator(byDay, Frequency.YEARLY == freq && byMonth.length == 0, start); + byDay = NO_DAYS; + } else { + if (Frequency.YEARLY == freq) { + monthGenerator = Generators.byMonthGenerator(new int[] { dtStart.month() }, start); + } + dayGenerator = Generators.byMonthDayGenerator(new int[] { dtStart.day() }, start); + } + break; + } + + if (secondGenerator == null) { + secondGenerator = Generators.bySecondGenerator(bySecond, start); + } + if (minuteGenerator == null) { + if (byMinute.length == 0 && freq.compareTo(Frequency.MINUTELY) < 0) { + minuteGenerator = Generators.serialMinuteGenerator(1, dtStart); + } else { + minuteGenerator = Generators.byMinuteGenerator(byMinute, start); + } + } + if (hourGenerator == null) { + if (byHour.length == 0 && freq.compareTo(Frequency.HOURLY) < 0) { + hourGenerator = Generators.serialHourGenerator(1, dtStart); + } else { + hourGenerator = Generators.byHourGenerator(byHour, start); + } + } + + if (dayGenerator == null) { + boolean dailyOrMoreOften = freq.compareTo(Frequency.DAILY) <= 0; + if (byMonthDay.length > 0) { + dayGenerator = Generators.byMonthDayGenerator(byMonthDay, start); + byMonthDay = NO_INTS; + } else if (byDay.length > 0) { + dayGenerator = Generators.byDayGenerator(byDay, Frequency.YEARLY == freq, start); + byDay = NO_DAYS; + } else if (dailyOrMoreOften) { + dayGenerator = Generators.serialDayGenerator(Frequency.DAILY == freq ? interval : 1, dtStart); + } else { + dayGenerator = Generators.byMonthDayGenerator(new int[] { dtStart.day() }, start); + } + } + + if (byDay.length > 0) { + filters.add(Filters.byDayFilter(byDay, Frequency.YEARLY == freq, wkst)); + byDay = NO_DAYS; + } + + if (byMonthDay.length > 0) { + filters.add(Filters.byMonthDayFilter(byMonthDay)); + } + + //generator inference common to all periods + if (byMonth.length > 0) { + monthGenerator = Generators.byMonthGenerator(byMonth, start); + } else if (monthGenerator == null) { + monthGenerator = Generators.serialMonthGenerator(freq == Frequency.MONTHLY ? interval : 1, dtStart); + } + + /* + * The condition tells the iterator when to halt. The condition is + * exclusive, so the date that triggers it will not be included. + */ + Predicate condition; + if (count != 0) { + condition = Conditions.countCondition(count); + + /* + * We can't shortcut because the countCondition must see every + * generated instance. + * + * TODO(msamuel): If count is large, we might try predicting the end + * date so that we can convert the COUNT condition to an UNTIL + * condition. + */ + canShortcutAdvance = false; + } else if (until != null) { + DateValue untilUtc; + if (until.hasTime()) { + TimeZone utc = TimeZone.getTimeZone("UTC"); + untilUtc = Google2445Utils.convert(until, utc); + } else { + //treat the ICalDate object as a timezone-less, calendar date + Calendar c = Calendar.getInstance(); + c.setTime(until); + untilUtc = new DateValueImpl( //@formatter:off + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH) + ); //@formatter:on + } + + if ((untilUtc instanceof TimeValue) != (dtStart instanceof TimeValue)) { + // TODO(msamuel): warn + if (dtStart instanceof TimeValue) { + untilUtc = TimeUtils.dayStart(untilUtc); + } else { + untilUtc = TimeUtils.toDateValue(untilUtc); + } + } + condition = Conditions.untilCondition(untilUtc); + } else { + condition = Predicates.alwaysTrue(); + } + + //combine filters into a single function + Predicate filter; + switch (filters.size()) { + case 0: + filter = Predicates. alwaysTrue(); + break; + case 1: + filter = filters.get(0); + break; + default: + filter = Predicates.and(filters); + break; + } + + Generator instanceGenerator; + if (bySetPos.length > 0) { + instanceGenerator = InstanceGenerators.bySetPosInstanceGenerator(bySetPos, freq, wkst, filter, yearGenerator, monthGenerator, dayGenerator, hourGenerator, minuteGenerator, secondGenerator); + } else { + instanceGenerator = InstanceGenerators.serialInstanceGenerator(filter, yearGenerator, monthGenerator, dayGenerator, hourGenerator, minuteGenerator, secondGenerator); + } + + return new RRuleIteratorImpl(dtStart, tzid, condition, instanceGenerator, yearGenerator, monthGenerator, dayGenerator, hourGenerator, minuteGenerator, secondGenerator, canShortcutAdvance); + } + + /** + * Generates a recurrence iterator that iterates over the union of the given + * recurrence iterators. + * @param first the first recurrence iterator + * @param rest the other recurrence iterators + * @return the union iterator + */ + public static RecurrenceIterator join(RecurrenceIterator first, RecurrenceIterator... rest) { + List all = new ArrayList(); + all.add(first); + all.addAll(Arrays.asList(rest)); + return new CompoundIteratorImpl(all, Collections. emptyList()); + } + + /** + *

+ * Generates a recurrence iterator that iterates over all the dates in a + * {@link RecurrenceIterator}, excluding those dates found in another + * {@link RecurrenceIterator}. + *

+ *

+ * Exclusions trump inclusions, and {@link DateValue dates} and + * {@link DateTimeValue date-times} never match one another. + *

+ * @param included the dates to include + * @param excluded the dates to exclude + * @return the resultant iterator + */ + public static RecurrenceIterator except(RecurrenceIterator included, RecurrenceIterator excluded) { + return new CompoundIteratorImpl(Collections.singleton(included), Collections.singleton(excluded)); + } + + /** + *

+ * Creates an optimized version of an array based on the given BYSETPOS + * array. + *

+ *

+ * For example, given the array BYMONTH=2,3,4,5 and a BYSETPOS + * of BYSETPOS=1,-1, this method will return + * BYMONTH=2,5. + *

+ * @param members the array to optimize + * @param bySetPos the BYSETPOS array + * @return the optimized array + */ + private static int[] filterBySetPos(int[] members, int[] bySetPos) { + members = Util.uniquify(members); + IntSet iset = new IntSet(); + for (int pos : bySetPos) { + if (pos == 0) { + continue; + } + if (pos < 0) { + pos += members.length; + } else { + --pos; // Zero-index. + } + if (pos >= 0 && pos < members.length) { + iset.add(members[pos]); + } + } + return iset.toIntArray(); + } + + /** + * Converts an {@link Integer} list to an int array. Null values are + * converted to zero. + * @param list the {@link Integer} list + * @return the int array + */ + private static int[] toIntArray(List list) { + int[] array = new int[list.size()]; + int i = 0; + for (Integer intObj : list) { + array[i++] = toInt(intObj); + } + return array; + } + + private static int toInt(Integer integer) { + return (integer == null) ? 0 : integer; + } + + private static final int[] NO_INTS = new int[0]; + private static final ByDay[] NO_DAYS = new ByDay[0]; + + private RecurrenceIteratorFactory() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/SingleValueGenerator.java b/app/src/main/java/biweekly/util/com/google/ical/iter/SingleValueGenerator.java new file mode 100644 index 0000000000..94c16d3c3f --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/SingleValueGenerator.java @@ -0,0 +1,61 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +/** + *

+ * A marker for {@link Generator}s that generate exactly one value per outer + * cycle. + *

+ *

+ * For example, {@code BYHOUR=3} generates exactly one hour per day and + * {@code BYMONTHDAY=12} generates exactly one day per month, but + * {@code BYHOUR=3,6} does not. Nor does {@code BYMONTHDAY=31}. + *

+ * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +abstract class SingleValueGenerator extends Generator { + /** + * Gets the single value that this generator generates. + * @return the value + */ + abstract int getValue(); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/ThrottledGenerator.java b/app/src/main/java/biweekly/util/com/google/ical/iter/ThrottledGenerator.java new file mode 100644 index 0000000000..70c33f0f1c --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/ThrottledGenerator.java @@ -0,0 +1,67 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +/** + *

+ * A generator that may stop generating values after some point (for example, if + * its output is never productive). + *

+ *

+ * This is used to stop rules like the one below from hanging an iterator. + *

+ * + *
+ * RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=30
+ * 
+ *

+ * If a rule does prove productive though, it can be alerted to the fact by the + * {@link #workDone} method, so that any throttle can be reset. + *

+ * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +abstract class ThrottledGenerator extends Generator { + /** + * Called to reset any throttle after work is done. This must be called in + * the outermost loop of any iterator. + */ + abstract void workDone(); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/iter/Util.java b/app/src/main/java/biweekly/util/com/google/ical/iter/Util.java new file mode 100644 index 0000000000..f27d7a883e --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/iter/Util.java @@ -0,0 +1,163 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.iter; + +import biweekly.util.ByDay; +import biweekly.util.DayOfWeek; +import biweekly.util.com.google.ical.util.DTBuilder; +import biweekly.util.com.google.ical.util.TimeUtils; +import biweekly.util.com.google.ical.values.DateValue; + +/** + * A dumping ground for utility functions that don't fit anywhere else. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +class Util { + /** + *

+ * Advances the given date to next date that falls on the given weekday. If + * the given date already falls on the given weekday, then the same date is + * returned. + *

+ *

+ * For example, if the date is a Thursday, and the week start is Monday, + * this method will return a date value that is set to the next Monday (4 + * days in the future). + *

+ * @param date the date + * @param weekday the day of the week that the week starts on + * @return the resultant date + */ + static DateValue nextWeekStart(DateValue date, DayOfWeek weekday) { + DTBuilder builder = new DTBuilder(date); + builder.day += (7 - ((7 + (TimeUtils.dayOfWeek(date).getCalendarConstant() - weekday.getCalendarConstant())) % 7)) % 7; + return builder.toDate(); + } + + /** + * Returns a sorted copy of an integer array with duplicate values removed. + * @param ints the integer array + * @return the sorted copy with duplicates removed + */ + static int[] uniquify(int[] ints) { + IntSet iset = new IntSet(); + for (int i : ints) { + iset.add(i); + } + return iset.toIntArray(); + } + + /** + * Given a weekday number, such as {@code -1SU}, this method calculates the + * day of the month that it falls on. The weekday number may be refer to a + * week in the current month in some contexts, or a week in the current year + * in other contexts. + * @param dow0 the day of week of the first day in the current year/month + * @param nDays the number of days in the current year/month (must be one of + * the following values [28,29,30,31,365,366]) + * @param weekNum the weekday number (for example, the -1 in {@code -1SU}) + * @param dow the day of the week (for example, the SU in {@code -1SU}) + * @param d0 the number of days between the first day of the current + * year/month and the current month + * @param nDaysInMonth the number of days in the current month + * @return the day of the month, or 0 if no such day exists + */ + static int dayNumToDate(DayOfWeek dow0, int nDays, int weekNum, DayOfWeek dow, int d0, int nDaysInMonth) { + //if dow is wednesday, then this is the date of the first wednesday + int firstDateOfGivenDow = 1 + ((7 + dow.getCalendarConstant() - dow0.getCalendarConstant()) % 7); + + int date; + if (weekNum > 0) { + date = ((weekNum - 1) * 7) + firstDateOfGivenDow - d0; + } else { //count weeks from end of month + //calculate last day of the given dow + //since nDays <= 366, this should be > nDays + int lastDateOfGivenDow = firstDateOfGivenDow + (7 * 54); + lastDateOfGivenDow -= 7 * ((lastDateOfGivenDow - nDays + 6) / 7); + date = lastDateOfGivenDow + 7 * (weekNum + 1) - d0; + } + return (date <= 0 || date > nDaysInMonth) ? 0 : date; + } + + /** + *

+ * Converts a relative week number (such as {@code -1SU}) to an absolute + * week number. + *

+ *

+ * For example, the week number {@code -1SU} refers to the last Sunday of + * either the month or year (depending on how this method was called). So if + * there are 5 Sundays in the given period, then given a week number of + * {@code -1SU}, this method would return 5. Similarly, {@code -2SU} would + * return 4. + *

+ * @param weekdayNum the weekday number (must be a negative value, such as + * {@code -1SU}) + * @param dow0 the day of the week of the first day of the week or month + * @param nDays the number of days in the month or year + * @return the absolute week number + */ + static int invertWeekdayNum(ByDay weekdayNum, DayOfWeek dow0, int nDays) { + //how many are there of that week? + return countInPeriod(weekdayNum.getDay(), dow0, nDays) + weekdayNum.getNum() + 1; + } + + /** + * Counts the number of occurrences of a weekday in a given period. + * @param dow the weekday + * @param dow0 the weekday of the first day of the period + * @param nDays the number of days in the period + */ + static int countInPeriod(DayOfWeek dow, DayOfWeek dow0, int nDays) { + //two cases: + // (1a) dow >= dow0: count === (nDays - (dow - dow0)) / 7 + // (1b) dow < dow0: count === (nDays - (7 - dow0 - dow)) / 7 + if (dow.getCalendarConstant() >= dow0.getCalendarConstant()) { + return 1 + ((nDays - (dow.getCalendarConstant() - dow0.getCalendarConstant()) - 1) / 7); + } else { + return 1 + ((nDays - (7 - (dow0.getCalendarConstant() - dow.getCalendarConstant())) - 1) / 7); + } + } + + private Util() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/util/DTBuilder.java b/app/src/main/java/biweekly/util/com/google/ical/util/DTBuilder.java new file mode 100644 index 0000000000..b2d119e185 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/util/DTBuilder.java @@ -0,0 +1,264 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.util; + +import biweekly.util.com.google.ical.values.DateTimeValue; +import biweekly.util.com.google.ical.values.DateTimeValueImpl; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + * A mutable buffer used to build {@link DateValue}s and {@link DateTimeValue}s. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public class DTBuilder { + /** + * The year. + */ + public int year; + + /** + * The month. This value is one-indexed, so "1" represents January. + */ + public int month; + + /** + * The day of the month. + */ + public int day; + + /** + * The hour. + */ + public int hour; + + /** + * The minute. + */ + public int minute; + + /** + * The second. + */ + public int second; + + /** + * Creates a new date builder. + * @param year the initial year + * @param month the initial month (this value is one-indexed, so "1" + * represents January) + * @param day the initial day + * @param hour the initial hour + * @param minute the initial minute + * @param second the initial second + */ + public DTBuilder(int year, int month, int day, int hour, int minute, int second) { + this.year = year; + this.month = month; + this.day = day; + this.hour = hour; + this.minute = minute; + this.second = second; + } + + /** + * Creates a new date builder. This constructor sets the time components to + * zero. + * @param year the initial year + * @param month the initial month (this value is one-indexed, so "1" + * represents January) + * @param day the initial day + */ + public DTBuilder(int year, int month, int day) { + this(year, month, day, 0, 0, 0); + } + + /** + * Creates a new date builder, initializing it to the given date value. + * @param date the date value to initialize the builder with + */ + public DTBuilder(DateValue date) { + this.year = date.year(); + this.month = date.month(); + this.day = date.day(); + if (date instanceof TimeValue) { + TimeValue tv = (TimeValue) date; + this.hour = tv.hour(); + this.minute = tv.minute(); + this.second = tv.second(); + } + } + + /** + * Produces a normalized date-time value, using zero for the time fields if + * none were provided. + * @return the date-time value + */ + public DateTimeValue toDateTime() { + normalize(); + return new DateTimeValueImpl(year, month, day, hour, minute, second); + } + + /** + * Produces a normalized date value. + * @return the date value + */ + public DateValue toDate() { + normalize(); + return new DateValueImpl(year, month, day); + } + + /** + *

+ * Compares the value of this builder to a given {@link DateValue}. Note + * that this method's behavior is undefined unless {@link #normalize} is + * called first. + *

+ *

+ * If you're not sure whether it's appropriate to use this method, use + * toDateValue().compareTo(dv) instead. + *

+ * @param date the date value to compare against + * @return a negative value if this date builder is less than the given date + * value, a positive value if this date builder is greater than the given + * date value, or zero if they are equal + */ + public int compareTo(DateValue date) { + long dvComparable = (((((long) date.year()) << 4) + date.month()) << 5) + date.day(); + long dtbComparable = ((((long) year << 4) + month << 5)) + day; + if (date instanceof TimeValue) { + TimeValue tv = (TimeValue) date; + dvComparable = (((((dvComparable << 5) + tv.hour()) << 6) + tv.minute()) << 6) + tv.second(); + dtbComparable = (((((dtbComparable << 5) + hour) << 6) + minute) << 6) + second; + } + long delta = dtbComparable - dvComparable; + return delta < 0 ? -1 : delta == 0 ? 0 : 1; + } + + /** + * Makes sure that the fields are in the proper ranges (for example, + * converts 32 January to 1 February, and 25:00:00 to 1:00:00 of the next + * day). + */ + public void normalize() { + normalizeTime(); + normalizeDate(); + } + + @Override + public String toString() { + return year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DTBuilder)) { + return false; + } + DTBuilder that = (DTBuilder) o; + return this.year == that.year && this.month == that.month && this.day == that.day && this.hour == that.hour && this.minute == that.minute && this.second == that.second; + } + + @Override + public int hashCode() { + return ((((((((year << 4) + month << 5) + day) << 5) + hour) << 6) + minute) << 6) + second; + } + + /** + * Makes sure that the time fields are in the proper ranges (for example, + * converts 25:00:00 to 1:00:00 of the next day). + */ + private void normalizeTime() { + int addMinutes = ((second < 0) ? (second - 59) : second) / 60; + second -= addMinutes * 60; + minute += addMinutes; + int addHours = ((minute < 0) ? (minute - 59) : minute) / 60; + minute -= addHours * 60; + hour += addHours; + int addDays = ((hour < 0) ? (hour - 23) : hour) / 24; + hour -= addDays * 24; + day += addDays; + } + + /** + * Makes sure that the date fields are in the proper ranges (for example, + * converts 32 January to 1 February). + */ + private void normalizeDate() { + while (day <= 0) { + int days = TimeUtils.yearLength(month > 2 ? year : year - 1); + day += days; + --year; + } + + if (month <= 0) { + int years = month / 12 - 1; + year += years; + month -= 12 * years; + } else if (month > 12) { + int years = (month - 1) / 12; + year += years; + month -= 12 * years; + } + + while (true) { + if (month == 1) { + int yearLength = TimeUtils.yearLength(year); + if (day > yearLength) { + ++year; + day -= yearLength; + } + } + + int monthLength = TimeUtils.monthLength(year, month); + if (day <= monthLength) { + break; + } + + day -= monthLength; + if (++month > 12) { + month -= 12; + ++year; + } + } + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/util/Predicate.java b/app/src/main/java/biweekly/util/com/google/ical/util/Predicate.java new file mode 100644 index 0000000000..98b9a35dc7 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/util/Predicate.java @@ -0,0 +1,45 @@ +// CopyrightGoogle Inc. All rights reserved. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.util; + +import java.io.Serializable; + +/** + * A function with a boolean return value. Useful for filtering. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + * @param the input type + */ +public interface Predicate extends Serializable { + /** + * Applies this predicate to the given object. + * @param input the input + * @return the value of this predicate when applied to the input + */ + boolean apply(T input); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/util/Predicates.java b/app/src/main/java/biweekly/util/com/google/ical/util/Predicates.java new file mode 100644 index 0000000000..0c83a5fa2c --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/util/Predicates.java @@ -0,0 +1,231 @@ +// CopyrightGoogle Inc. All rights reserved. + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.util; + +import java.util.Collection; + +/** + * Static methods for creating the standard set of {@link Predicate} objects. + * @author mikesamuel+svn@gmail.com (Mike Samuel) + * @author Michael Angstadt + */ +public class Predicates { + private static final Predicate ALWAYS_TRUE = new AlwaysTruePredicate(); + private static final Predicate ALWAYS_FALSE = new AlwaysFalsePredicate(); + + /** + * Returns a predicate that always evaluates to true. + * @param the input type + * @return the predicate + */ + @SuppressWarnings("unchecked") + public static Predicate alwaysTrue() { + return (Predicate) ALWAYS_TRUE; + } + + /** + * Returns a predicate that always evaluates to false. + * @param the input type + * @return the predicate + */ + @SuppressWarnings("unchecked") + public static Predicate alwaysFalse() { + return (Predicate) ALWAYS_FALSE; + } + + /** + * Returns a predicate that evaluates to true iff the given predicate + * evaluates to false. + * @param predicate the predicate to evaluate + * @param the input type + * @return the resultant predicate + */ + public static Predicate not(Predicate predicate) { + return new NotPredicate(predicate); + } + + /** + * Returns a predicate that evaluates to true iff each of its components + * evaluates to true. The components are evaluated in order, and evaluation + * will be "short-circuited" as soon as the answer is determined. + * @param components the predicates to evaluate + * @param the input type + * @return the resultant predicate + */ + public static Predicate and(Predicate... components) { + components = components.clone(); + int n = components.length; + for (int i = 0; i < n; ++i) { + Predicate p = components[i]; + if (p == ALWAYS_FALSE) { + return alwaysFalse(); + } + if (p == ALWAYS_TRUE) { + components[i] = components[n - 1]; + --i; + --n; + } + } + if (n == 0) { + return alwaysTrue(); + } + if (n != components.length) { + @SuppressWarnings("unchecked") + Predicate[] newComponents = new Predicate[n]; + System.arraycopy(newComponents, 0, components, 0, n); + components = newComponents; + } + return new AndPredicate(components); + } + + /** + * Returns a predicate that evaluates to true iff each of its components + * evaluates to true. The components are evaluated in order, and evaluation + * will be "short-circuited" as soon as the answer is determined. + * @param components the predicates to evaluate + * @param the input type + * @return the resultant predicate + */ + @SuppressWarnings("unchecked") + public static Predicate and(Collection> components) { + return and(components.toArray(new Predicate[0])); + } + + /** + * Returns a predicate that evaluates to true iff any one of its components + * evaluates to true. The components are evaluated in order, and evaluation + * will be "short-circuited" as soon as the answer is determined. + * @param components the predicates to evaluate + * @param the input type + * @return the resultant predicate + */ + public static Predicate or(Predicate... components) { + components = components.clone(); + int n = components.length; + for (int i = 0; i < n; ++i) { + Predicate p = components[i]; + if (p == ALWAYS_TRUE) { + return alwaysTrue(); + } + if (p == ALWAYS_FALSE) { + components[i] = components[n - 1]; + --i; + --n; + } + } + if (n == 0) { + return alwaysFalse(); + } + if (n != components.length) { + @SuppressWarnings("unchecked") + Predicate[] newComponents = new Predicate[n]; + System.arraycopy(newComponents, 0, components, 0, n); + components = newComponents; + } + return new OrPredicate(components); + } + + private static class AlwaysTruePredicate implements Predicate { + private static final long serialVersionUID = 8759914710239461322L; + + public boolean apply(T t) { + return true; + } + + @Override + public String toString() { + return "true"; + } + } + + private static class AlwaysFalsePredicate implements Predicate { + private static final long serialVersionUID = -565481022115659695L; + + public boolean apply(T t) { + return false; + } + + @Override + public String toString() { + return "false"; + } + } + + private static class NotPredicate implements Predicate { + private static final long serialVersionUID = -5113445916422049953L; + private final Predicate predicate; + + private NotPredicate(Predicate predicate) { + this.predicate = predicate; + } + + public boolean apply(T t) { + return !predicate.apply(t); + } + } + + private static class AndPredicate implements Predicate { + private static final long serialVersionUID = 1022358602593297546L; + private final Predicate[] components; + + private AndPredicate(Predicate... components) { + this.components = components; + } + + public boolean apply(T t) { + for (Predicate predicate : components) { + if (!predicate.apply(t)) { + return false; + } + } + return true; + } + } + + private static class OrPredicate implements Predicate { + private static final long serialVersionUID = -7942366790698074803L; + private final Predicate[] components; + + private OrPredicate(Predicate... components) { + this.components = components; + } + + public boolean apply(T t) { + for (Predicate predicate : components) { + if (predicate.apply(t)) { + return true; + } + } + return false; + } + } + + private Predicates() { + //uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/util/TimeUtils.java b/app/src/main/java/biweekly/util/com/google/ical/util/TimeUtils.java new file mode 100644 index 0000000000..8581d8b02e --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/util/TimeUtils.java @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * All Rights Reserved. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.util; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.SimpleTimeZone; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import biweekly.util.DayOfWeek; +import biweekly.util.com.google.ical.values.DateTimeValue; +import biweekly.util.com.google.ical.values.DateTimeValueImpl; +import biweekly.util.com.google.ical.values.DateValue; +import biweekly.util.com.google.ical.values.DateValueImpl; +import biweekly.util.com.google.ical.values.TimeValue; + +/** + * Utility methods for working with times and dates. + * @author Neal Gafter + * @author Michael Angstadt + */ +public class TimeUtils { + private static TimeZone ZULU = new SimpleTimeZone(0, "Etc/GMT"); + + /** + * Gets the UTC timezone. + * @return the UTC timezone + */ + public static TimeZone utcTimezone() { + return ZULU; + } + + /** + * Get a "time_t" in milliseconds given a number of seconds since the + * Dershowitz/Reingold epoch relative to a given timezone. + * @param epochSecs the number of seconds since the Dershowitz/Reingold + * epoch relative to the given timezone + * @param zone timezone against which epochSecs applies + * @return the number of milliseconds since 00:00:00 Jan 1, 1970 GMT + */ + private static long timetMillisFromEpochSecs(long epochSecs, TimeZone zone) { + DateTimeValue date = timeFromSecsSinceEpoch(epochSecs); + Calendar cal = new GregorianCalendar(zone); + cal.clear(); + cal.set(date.year(), date.month() - 1, date.day(), date.hour(), date.minute(), date.second()); + return cal.getTimeInMillis(); + } + + private static DateTimeValue convert(DateTimeValue time, TimeZone zone, int sense) { + if (zone == null || zone.hasSameRules(ZULU) || time.year() == 0) { + return time; + } + + TimeZone epochTz, dateTimeValueTz; + if (sense > 0) { + //time is in UTC; convert to time in zone provided + epochTz = ZULU; + dateTimeValueTz = zone; + } else { + //time is in local time; convert to UTC + epochTz = zone; + dateTimeValueTz = ZULU; + } + + long epochSeconds = secsSinceEpoch(time); + long timetMillis = timetMillisFromEpochSecs(epochSeconds, epochTz); + return toDateTimeValue(timetMillis, dateTimeValueTz); + } + + /** + * Converts a {@link DateValue} from UTC to another timezone. + * @param date the date value (in UTC) + * @param zone the timezone to convert to + * @return the converted date value + */ + public static DateValue fromUtc(DateValue date, TimeZone zone) { + return (date instanceof DateTimeValue) ? fromUtc((DateTimeValue) date, zone) : date; + } + + /** + * Converts a {@link DateTimeValue} from UTC to another timezone. + * @param date the date-time value (in UTC) + * @param zone the timezone to convert to + * @return the converted date-time value + */ + public static DateTimeValue fromUtc(DateTimeValue date, TimeZone zone) { + return convert(date, zone, +1); + } + + /** + * Converts a {@link DateValue} to UTC. + * @param date the date value + * @param zone the timezone the date value is in + * @return the converted date value + */ + public static DateValue toUtc(DateValue date, TimeZone zone) { + return (date instanceof TimeValue) ? convert((DateTimeValue) date, zone, -1) : date; + } + + /** + * Adds a duration to a date. + * @param date the date + * @param duration the duration to add to the date + * @return the result + */ + public static DateValue add(DateValue date, DateValue duration) { + DTBuilder db = new DTBuilder(date); + db.year += duration.year(); + db.month += duration.month(); + db.day += duration.day(); + if (duration instanceof TimeValue) { + TimeValue tdur = (TimeValue) duration; + db.hour += tdur.hour(); + db.minute += tdur.minute(); + db.second += tdur.second(); + return db.toDateTime(); + } + return (date instanceof TimeValue) ? db.toDateTime() : db.toDate(); + } + + /** + * Calculates the number of days between two dates. + * @param date1 the first date + * @param date2 the second date + * @return the number of days + */ + public static int daysBetween(DateValue date1, DateValue date2) { + return fixedFromGregorian(date1) - fixedFromGregorian(date2); + } + + /** + * Calculates the number of days between two dates. + * @param year1 the year of the first date + * @param month1 the month of the first date + * @param day1 the day of the first date + * @param year2 the year of the second date + * @param month2 the month of the second date + * @param day2 the day of the second date + * @return the number of days + */ + public static int daysBetween(int year1, int month1, int day1, int year2, int month2, int day2) { + return fixedFromGregorian(year1, month1, day1) - fixedFromGregorian(year2, month2, day2); + } + + /** + *

+ * Calculates the number of days since the epoch. + *

+ *

+ * This is the imaginary beginning of year zero in a hypothetical backward + * extension of the Gregorian calendar through time. See + * "Calendrical Calculations" by Reingold and Dershowitz. + *

+ * @param date the date to start from + * @return the number of days + */ + private static int fixedFromGregorian(DateValue date) { + return fixedFromGregorian(date.year(), date.month(), date.day()); + } + + /** + *

+ * Calculates the number of days since the epoch. + *

+ *

+ * This is the imaginary beginning of year zero in a hypothetical backward + * extension of the Gregorian calendar through time. See + * "Calendrical Calculations" by Reingold and Dershowitz. + *

+ * @param year the year of the date to start from + * @param month the month of the date to start from + * @param day the day of the date to start from + * @return the number of days + */ + public static int fixedFromGregorian(int year, int month, int day) { + int yearM1 = year - 1; + return 365 * yearM1 + yearM1 / 4 - yearM1 / 100 + yearM1 / 400 + (367 * month - 362) / 12 + (month <= 2 ? 0 : isLeapYear(year) ? -1 : -2) + day; + } + + /** + * Determines if a year is a leap year. + * @param year the year + * @return true if it's a leap year, false if not + */ + public static boolean isLeapYear(int year) { + return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); + } + + /** + * Determines how many days are in a year. + * @param year the year + * @return the number of days + */ + public static int yearLength(int year) { + return isLeapYear(year) ? 366 : 365; + } + + /** + * Calculates the number of days in a month. + * @param year the year + * @param month the month (in range [1,12]) + * @return the number of days + */ + public static int monthLength(int year, int month) { + switch (month) { + case 1: + case 3: + case 5: + case 7: + case 8: + case 10: + case 12: + return 31; + case 4: + case 6: + case 9: + case 11: + return 30; + case 2: + return isLeapYear(year) ? 29 : 28; + default: + throw new AssertionError(month); + } + } + + private static int[] MONTH_START_TO_DOY = new int[12]; + static { + for (int m = 1; m < 12; ++m) { + MONTH_START_TO_DOY[m] = MONTH_START_TO_DOY[m - 1] + monthLength(1970, m); + } + } + + /** + * Gets the day of the year for a given date. + * @param year the date's year + * @param month the date's month + * @param date the date's day + * @return the day of the year (in range [0-365]) + */ + public static int dayOfYear(int year, int month, int date) { + int leapAdjust = month > 2 && isLeapYear(year) ? 1 : 0; + return MONTH_START_TO_DOY[month - 1] + leapAdjust + date - 1; + } + + private static final DayOfWeek[] DAYS_OF_WEEK = DayOfWeek.values(); + + /** + * Gets the day of the week the given date falls on. + * @param date the date + * @return the day of the week + */ + public static DayOfWeek dayOfWeek(DateValue date) { + int dayIndex = fixedFromGregorian(date.year(), date.month(), date.day()) % 7; + if (dayIndex < 0) { + dayIndex += 7; + } + return DAYS_OF_WEEK[dayIndex]; + } + + /** + * Gets the day of the week of the first day in the given month. + * @param year the year + * @param month the month (1-12) + * @return the day of the week + */ + public static DayOfWeek firstDayOfWeekInMonth(int year, int month) { + int result = fixedFromGregorian(year, month, 1) % 7; + if (result < 0) { + result += 7; + } + return DAYS_OF_WEEK[result]; + } + + /** + * Computes the gregorian time from the number of seconds since the + * Proleptic Gregorian Epoch. See "Calendrical Calculations", Reingold and + * Dershowitz. + * @param secsSinceEpoch the number of seconds since the epoch + * @return the gregorian time + */ + public static DateTimeValue timeFromSecsSinceEpoch(long secsSinceEpoch) { + // TODO: should we handle -ve years? + int secsInDay = (int) (secsSinceEpoch % SECS_PER_DAY); + int daysSinceEpoch = (int) (secsSinceEpoch / SECS_PER_DAY); + int approx = (int) ((daysSinceEpoch + 10) * 400L / 146097); + int year = (daysSinceEpoch >= fixedFromGregorian(approx + 1, 1, 1)) ? approx + 1 : approx; + int jan1 = fixedFromGregorian(year, 1, 1); + int priorDays = daysSinceEpoch - jan1; + int march1 = fixedFromGregorian(year, 3, 1); + int correction = (daysSinceEpoch < march1) ? 0 : isLeapYear(year) ? 1 : 2; + int month = (12 * (priorDays + correction) + 373) / 367; + int month1 = fixedFromGregorian(year, month, 1); + int day = daysSinceEpoch - month1 + 1; + int second = secsInDay % 60; + int minutesInDay = secsInDay / 60; + int minute = minutesInDay % 60; + int hour = minutesInDay / 60; + if (!(hour >= 0 && hour < 24)) { + throw new AssertionError("Input was: " + secsSinceEpoch + "to make hour: " + hour); + } + return new DateTimeValueImpl(year, month, day, hour, minute, second); + } + + private static final long SECS_PER_DAY = 60L * 60 * 24; + + /** + * Computes the number of seconds from the Proleptic Gregorian epoch to the + * given time. See "Calendrical Calculations", Reingold and Dershowitz. + * @param date the date + * @return the number of seconds + */ + public static long secsSinceEpoch(DateValue date) { + long result = fixedFromGregorian(date) * SECS_PER_DAY; + if (date instanceof TimeValue) { + TimeValue time = (TimeValue) date; + result += time.second() + 60 * (time.minute() + 60 * time.hour()); + } + return result; + } + + /** + * Builds a date-time value that represents the start of the given day (all + * time components set to 0). + * @param date the day + * @return the date-time value + */ + public static DateTimeValue dayStart(DateValue date) { + return new DateTimeValueImpl(date.year(), date.month(), date.day(), 0, 0, 0); + } + + /** + * Converts the given date to a {@link DateValue} object if it is a + * {@link TimeValue} instance. If it is not a {@link TimeValue} instance, + * then the same date object is returned unchanged. + * @param date the date + * @return the date value + */ + public static DateValue toDateValue(DateValue date) { + return (date instanceof TimeValue) ? new DateValueImpl(date.year(), date.month(), date.day()) : date; + } + + private static final TimeZone BOGUS_TIMEZONE = TimeZone.getTimeZone("noSuchTimeZone"); + + private static final Pattern UTC_TZID = Pattern.compile("^GMT([+-]0(:00)?)?$|UTC|Zulu|Etc\\/GMT|Greenwich.*", Pattern.CASE_INSENSITIVE); + + /** + * Returns the timezone with the given name or null if no such timezone. + * @param tzString the timezone name (e.g. "America/New_York") + * @return the timezone or null if no such timezone exists + */ + public static TimeZone timeZoneForName(String tzString) { + TimeZone tz = TimeZone.getTimeZone(tzString); + if (!tz.hasSameRules(BOGUS_TIMEZONE)) { + return tz; + } + + /* + * See if the user really was asking for GMT because if + * TimeZone.getTimeZone can't recognize tzString, then that is what it + * will return. + */ + Matcher m = UTC_TZID.matcher(tzString); + return m.matches() ? TimeUtils.utcTimezone() : null; + } + + /** + * Builds a {@link DateTimeValue} object from the given data. + * @param millisFromEpoch the number of milliseconds from the epoch + * @param zone the timezone the number of milliseconds is in + * @return the {@link DateTimeValue} object + */ + public static DateTimeValue toDateTimeValue(long millisFromEpoch, TimeZone zone) { + GregorianCalendar c = new GregorianCalendar(zone); + c.clear(); + c.setTimeInMillis(millisFromEpoch); + //@formatter:off + return new DateTimeValueImpl ( + c.get(Calendar.YEAR), + c.get(Calendar.MONTH) + 1, + c.get(Calendar.DAY_OF_MONTH), + c.get(Calendar.HOUR_OF_DAY), + c.get(Calendar.MINUTE), + c.get(Calendar.SECOND) + ); + //@formatter:on + } + + private static final TimeValue MIDNIGHT = new TimeValue() { + public int hour() { + return 0; + } + + public int minute() { + return 0; + } + + public int second() { + return 0; + } + }; + + /** + * Gets the time component of a date value. + * @param date the date value + * @return the date value's time component or a time value representing + * midnight if the date value does not have a time component + */ + public static TimeValue timeOf(DateValue date) { + return (date instanceof TimeValue) ? (TimeValue) date : MIDNIGHT; + } + + private TimeUtils() { + // uninstantiable + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/values/DateTimeValue.java b/app/src/main/java/biweekly/util/com/google/ical/values/DateTimeValue.java new file mode 100644 index 0000000000..6dd747b13d --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/values/DateTimeValue.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * All Rights Reserved. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.values; + +/** + * An instant in time. + * @author Neal Gafter + * @author Michael Angstadt + */ +public interface DateTimeValue extends DateValue, TimeValue { + //simple union +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/values/DateTimeValueImpl.java b/app/src/main/java/biweekly/util/com/google/ical/values/DateTimeValueImpl.java new file mode 100644 index 0000000000..c73bd9593e --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/values/DateTimeValueImpl.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * All Rights Reserved. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.values; + +/** + * An instant in time. + * @author Neal Gafter + * @author Michael Angstadt + */ +public class DateTimeValueImpl extends DateValueImpl implements DateTimeValue { + private final int hour, minute, second; + + /** + * Creates a new date-time value. + * @param year the year + * @param month the month (1-12) + * @param day the day (1-31) + * @param hour the hour (0-24) + * @param minute the minute (0-59) + * @param second the second (0-59) + */ + public DateTimeValueImpl(int year, int month, int day, int hour, int minute, int second) { + super(year, month, day); + this.hour = hour; + this.minute = minute; + this.second = second; + } + + public int hour() { + return hour; + } + + public int minute() { + return minute; + } + + public int second() { + return second; + } + + @Override + public int hashCode() { + return super.hashCode() ^ ((hour << 12) + (minute << 6) + second); + } + + @Override + public String toString() { + return String.format("%sT%02d%02d%02d", super.toString(), hour, minute, second); + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/values/DateValue.java b/app/src/main/java/biweekly/util/com/google/ical/values/DateValue.java new file mode 100644 index 0000000000..a2315839d1 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/values/DateValue.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * All Rights Reserved. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.values; + +/** + * A calendar date. + * @author Neal Gafter + * @author Michael Angstadt + */ +public interface DateValue extends Comparable { + /** + * Gets the Gregorian year (for example, 2004). + * @return the year + */ + int year(); + + /** + * Gets the Gregorian month (in the range 1-12). + * @return the month + */ + int month(); + + /** + * Gets the Gregorian day of the month (in the range 1-31). + * @return the day of the month + */ + int day(); +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/values/DateValueImpl.java b/app/src/main/java/biweekly/util/com/google/ical/values/DateValueImpl.java new file mode 100644 index 0000000000..dce6934236 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/values/DateValueImpl.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * All Rights Reserved. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.values; + +/** + * A calendar date. + * @author Neal Gafter + * @author Michael Angstadt + */ +public class DateValueImpl implements DateValue { + private final int year, month, day; + + /** + * Creates a new date value. + * @param year the year + * @param month the month (1-12) + * @param day the day (1-31) + */ + public DateValueImpl(int year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + public int year() { + return year; + } + + public int month() { + return month; + } + + public int day() { + return day; + } + + @Override + public String toString() { + return String.format("%04d%02d%02d", year, month, day); + } + + public final int compareTo(DateValue other) { + //@formatter:off + int n0 = day() + //5 bits + (month() << 5) + //4 bits + (year() << 9); + int n1 = other.day() + + (other.month() << 5) + + (other.year() << 9); + //@formatter:on + + if (n0 != n1) { + return n0 - n1; + } + + if (!(this instanceof TimeValue)) { + return (other instanceof TimeValue) ? -1 : 0; + } + + TimeValue self = (TimeValue) this; + if (!(other instanceof TimeValue)) { + return 1; + } + + TimeValue othr = (TimeValue) other; + //@formatter:off + int m0 = self.second() + //6 bits + (self.minute() << 6) + //6 bits + (self.hour() << 12); + int m1 = othr.second() + + (othr.minute() << 6) + + (othr.hour() << 12); + //@formatter:on + return m0 - m1; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DateValue)) { + return false; + } + return compareTo((DateValue) o) == 0; + } + + @Override + public int hashCode() { + return (year() << 9) + (month() << 5) + day(); + } +} diff --git a/app/src/main/java/biweekly/util/com/google/ical/values/TimeValue.java b/app/src/main/java/biweekly/util/com/google/ical/values/TimeValue.java new file mode 100644 index 0000000000..d705df48e6 --- /dev/null +++ b/app/src/main/java/biweekly/util/com/google/ical/values/TimeValue.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * All Rights Reserved. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.com.google.ical.values; + +/** + * A time of day. + * @author Neal Gafter + * @author Michael Angstadt + */ +public interface TimeValue { + /** + * Gets the hour (in the range 0-24). + * @return the hour + */ + int hour(); + + /** + * Gets the minute (in the range 0-59). If the hour is 24, then this method + * should return zero. + * @return the minute + */ + int minute(); + + /** + * Gets the second (in the range 0 through 59). If the hour is 24, then this + * method should return zero. + * @return the second + */ + int second(); +} diff --git a/app/src/main/java/biweekly/util/org/apache/commons/codec/DecoderException.java b/app/src/main/java/biweekly/util/org/apache/commons/codec/DecoderException.java new file mode 100644 index 0000000000..6cb03f423e --- /dev/null +++ b/app/src/main/java/biweekly/util/org/apache/commons/codec/DecoderException.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package biweekly.util.org.apache.commons.codec; + +/** + * Thrown when there is a failure condition during the decoding process. This exception is thrown when a {@link Decoder} + * encounters a decoding specific exception such as invalid data, or characters outside of the expected range. + * + * @version $Id: DecoderException.java 1619948 2014-08-22 22:53:55Z ggregory $ + */ +public class DecoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public DecoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + */ + public DecoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/biweekly/util/org/apache/commons/codec/EncoderException.java b/app/src/main/java/biweekly/util/org/apache/commons/codec/EncoderException.java new file mode 100644 index 0000000000..7b2b155479 --- /dev/null +++ b/app/src/main/java/biweekly/util/org/apache/commons/codec/EncoderException.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package biweekly.util.org.apache.commons.codec; + +/** + * Thrown when there is a failure condition during the encoding process. This exception is thrown when an + * {@link Encoder} encounters a encoding specific exception such as invalid data, inability to calculate a checksum, + * characters outside of the expected range. + * + * @version $Id: EncoderException.java 1619948 2014-08-22 22:53:55Z ggregory $ + */ +public class EncoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public EncoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * a useful message relating to the encoder specific error. + */ + public EncoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + *

+ * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/biweekly/util/org/apache/commons/codec/binary/Base64.java b/app/src/main/java/biweekly/util/org/apache/commons/codec/binary/Base64.java new file mode 100644 index 0000000000..c79909f258 --- /dev/null +++ b/app/src/main/java/biweekly/util/org/apache/commons/codec/binary/Base64.java @@ -0,0 +1,815 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.org.apache.commons.codec.binary; + +import java.math.BigInteger; + +/** + * Provides Base64 encoding and decoding as defined by RFC 2045. + * + *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ *

+ * The class can be parameterized in the following manner with various constructors: + *

+ *
    + *
  • URL-safe mode: Default off.
  • + *
  • Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of + * 4 in the encoded data. + *
  • Line separator: Default is CRLF ("\r\n")
  • + *
+ *

+ * The URL-safe parameter is only applied to encode operations. Decoding seamlessly handles both modes. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only + * encode/decode character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, + * UTF-8, etc). + *

+ *

+ * This class is thread-safe. + *

+ *

+ * biweekly integration note: Minor modifications were made to this class so that it could be + * incorporated into the biweekly code base. Defining the Apache Commons Codec library as a project dependency causes + * an issue with Android devices, which is why parts of its source code have been directly incorporated into the + * biweekly code base. + *

+ * + * @see RFC 2045 + * @since 1.0 + * @version $Id: Base64.java 1635986 2014-11-01 16:27:52Z tn $ + */ +public class Base64 extends BaseNCodec { + + /** + * BASE32 characters are 6 bits in length. + * They are formed by taking a block of 3 octets to form a 24-bit string, + * which is converted into 4 BASE64 characters. + */ + private static final int BITS_PER_ENCODED_BYTE = 6; + private static final int BYTES_PER_UNENCODED_BLOCK = 3; + private static final int BYTES_PER_ENCODED_BLOCK = 4; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + *

+ * N.B. The next major release may break compatibility and make this field private. + *

+ * + * @see RFC 2045 section 2.1 + */ + static final byte[] CHUNK_SEPARATOR = {'\r', '\n'}; + + /** + * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet" + * equivalents as specified in Table 1 of RFC 2045. + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] STANDARD_ENCODE_TABLE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + /** + * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / + * changed to - and _ to make the encoded Base64 results more URL-SAFE. + * This table is only used when the Base64's mode is set to URL-SAFE. + */ + private static final byte[] URL_SAFE_ENCODE_TABLE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + }; + + /** + * This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified + * in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64 + * alphabet but fall within the bounds of the array are translated to -1. + * + * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both + * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit). + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] DECODE_TABLE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }; + + /** + * Base64 uses 6-bit fields. + */ + /** Mask used to extract 6 bits, used when encoding */ + private static final int MASK_6BITS = 0x3f; + + // The static final fields above are used for the original static byte[] methods on Base64. + // The private member fields below are used with the new streaming approach, which requires + // some state be preserved between calls of encode() and decode(). + + /** + * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able + * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch + * between the two modes. + */ + private final byte[] encodeTable; + + // Only one decode table currently; keep for consistency with Base32 code + private final byte[] decodeTable = DECODE_TABLE; + + /** + * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * decodeSize = 3 + lineSeparator.length; + */ + private final int decodeSize; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * encodeSize = 4 + lineSeparator.length; + */ + private final int encodeSize; + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ */ + public Base64() { + this(0); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode. + *

+ * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ * + * @param urlSafe + * if true, URL-safe encoding is used. In most cases this should be set to + * false. + * @since 1.4 + */ + public Base64(final boolean urlSafe) { + this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @since 1.4 + */ + public Base64(final int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @throws IllegalArgumentException + * Thrown when the provided lineSeparator included some base64 characters. + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator) { + this(lineLength, lineSeparator, false); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @param urlSafe + * Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode + * operations. Decoding seamlessly handles both modes. + * Note: no padding is added when using the URL-safe alphabet. + * @throws IllegalArgumentException + * The provided lineSeparator included some base64 characters. That's not going to work! + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) { + super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, + lineLength, + lineSeparator == null ? 0 : lineSeparator.length); + // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0 + // @see test case Base64Test.testConstructors() + if (lineSeparator != null) { + if (containsAlphabetOrPad(lineSeparator)) { + final String sep = newStringUtf8(lineSeparator); + throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]"); + } + if (lineLength > 0){ // null line-sep forces no chunking rather than throwing IAE + this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length; + this.lineSeparator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length); + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + this.decodeSize = this.encodeSize - 1; + this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; + } + + /** + * Returns our current encode mode. True if we're URL-SAFE, false otherwise. + * + * @return true if we're in URL-SAFE mode, false otherwise. + * @since 1.4 + */ + public boolean isUrlSafe() { + return this.encodeTable == URL_SAFE_ENCODE_TABLE; + } + + /** + *

+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with + * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last + * remaining bytes (if not multiple of 3). + *

+ *

Note: no padding is added when encoding using the URL-safe alphabet.

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in + * byte[] array of binary data to base64 encode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context + * the context to be used + */ + @Override + void encode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + context.eof = true; + if (0 == context.modulus && lineLength == 0) { + return; // no leftovers to process and not using chunking + } + final byte[] buffer = ensureBufferSize(encodeSize, context); + final int savedPos = context.pos; + switch (context.modulus) { // 0-2 + case 0 : // nothing to do here + break; + case 1 : // 8 bits = 6 + 2 + // top 6 bits: + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS]; + // remaining 2: + buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + } + break; + + case 2 : // 16 bits = 6 + 6 + 4 + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + } + break; + default: + throw new IllegalStateException("Impossible modulus "+context.modulus); + } + context.currentLinePos += context.pos - savedPos; // keep track of current line position + // if currentPos == 0 we are at the start of a line, so don't add CRLF + if (lineLength > 0 && context.currentLinePos > 0) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(encodeSize, context); + context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK; + int b = in[inPos++]; + if (b < 0) { + b += 256; + } + context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE + if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS]; + context.currentLinePos += BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0 && lineLength <= context.currentLinePos) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + context.currentLinePos = 0; + } + } + } + } + } + + /** + *

+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once + * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" + * call is not necessary when decoding, but it doesn't hurt, either. + *

+ *

+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are + * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in, + * garbage-out philosophy: it will not check the provided data for validity. + *

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in + * byte[] array of ascii data to base64 decode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context + * the context to be used + */ + @Override + void decode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + if (inAvail < 0) { + context.eof = true; + } + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + final byte b = in[inPos++]; + if (b == pad) { + // We're done. + context.eof = true; + break; + } else { + if (b >= 0 && b < DECODE_TABLE.length) { + final int result = DECODE_TABLE[b]; + if (result >= 0) { + context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK; + context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result; + if (context.modulus == 0) { + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); + } + } + } + } + } + + // Two forms of EOF as far as base64 decoder is concerned: actual + // EOF (-1) and first time '=' character is encountered in stream. + // This approach makes the '=' padding characters completely optional. + if (context.eof && context.modulus != 0) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + + // We have some spare bits remaining + // Output all whole multiples of 8 bits and ignore the rest + switch (context.modulus) { +// case 0 : // impossible, as excluded above + case 1 : // 6 bits - ignore entirely + // TODO not currently tested; perhaps it is impossible? + break; + case 2 : // 12 bits = 8 + 4 + context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits + buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); + break; + case 3 : // 18 bits = 8 + 8 + 2 + context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); + break; + default: + throw new IllegalStateException("Impossible modulus "+context.modulus); + } + } + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param arrayOctet + * byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is empty; + * false, otherwise + * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0. + */ + @Deprecated + public static boolean isArrayByteBase64(final byte[] arrayOctet) { + return isBase64(arrayOctet); + } + + /** + * Returns whether or not the octet is in the base 64 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the base 64 alphabet, false otherwise. + * @since 1.4 + */ + public static boolean isBase64(final byte octet) { + return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1); + } + + /** + * Tests a given String to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param base64 + * String to test + * @return true if all characters in the String are valid characters in the Base64 alphabet or if + * the String is empty; false, otherwise + * @since 1.5 + */ + public static boolean isBase64(final String base64) { + return isBase64(getBytesUtf8(base64)); + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param arrayOctet + * byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is empty; + * false, otherwise + * @since 1.5 + */ + public static boolean isBase64(final byte[] arrayOctet) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) { + return false; + } + } + return true; + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * @param binaryData + * binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 representation. + */ + public static byte[] encodeBase64(final byte[] binaryData) { + return encodeBase64(binaryData, false); + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * NOTE: We changed the behaviour of this method from multi-line chunking (commons-codec-1.4) to + * single-line non-chunking (commons-codec-1.5). + * + * @param binaryData + * binary data to encode + * @return String containing Base64 characters. + * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not). + */ + public static String encodeBase64String(final byte[] binaryData) { + return newStringUtf8(encodeBase64(binaryData, false)); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * @param binaryData + * binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 representation. + * @since 1.4 + */ + public static byte[] encodeBase64URLSafe(final byte[] binaryData) { + return encodeBase64(binaryData, false, true); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * @param binaryData + * binary data to encode + * @return String containing Base64 characters + * @since 1.4 + */ + public static String encodeBase64URLSafeString(final byte[] binaryData) { + return newStringUtf8(encodeBase64(binaryData, false, true)); + } + + /** + * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks + * + * @param binaryData + * binary data to encode + * @return Base64 characters chunked in 76 character blocks + */ + public static byte[] encodeBase64Chunked(final byte[] binaryData) { + return encodeBase64(binaryData, true); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) { + return encodeBase64(binaryData, isChunked, false); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe + * if true this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) { + return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe + * if true this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @param maxResultSize + * The maximum result size to accept. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than maxResultSize + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, + final boolean urlSafe, final int maxResultSize) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + + // Create this so can use the super-class method + // Also ensures that the same roundings are performed by the ctor and the code + final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe); + final long len = b64.getEncodedLength(binaryData); + if (len > maxResultSize) { + throw new IllegalArgumentException("Input array too big, the output array would be bigger (" + + len + + ") than the specified maximum size of " + + maxResultSize); + } + + return b64.encode(binaryData); + } + + /** + * Decodes a Base64 String into octets. + *

+ * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

+ * + * @param base64String + * String containing Base64 data + * @return Array containing decoded data. + * @since 1.4 + */ + public static byte[] decodeBase64(final String base64String) { + return new Base64().decode(base64String); + } + + /** + * Decodes Base64 data into octets. + *

+ * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

+ * + * @param base64Data + * Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase64(final byte[] base64Data) { + return new Base64().decode(base64Data); + } + + // Implementation of the Encoder Interface + + // Implementation of integer encoding used for crypto + /** + * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature. + * + * @param pArray + * a byte array containing base64 character data + * @return A BigInteger + * @since 1.4 + */ + public static BigInteger decodeInteger(final byte[] pArray) { + return new BigInteger(1, decodeBase64(pArray)); + } + + /** + * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature. + * + * @param bigInt + * a BigInteger + * @return A byte array containing base64 character data + * @throws NullPointerException + * if null is passed in + * @since 1.4 + */ + public static byte[] encodeInteger(final BigInteger bigInt) { + if (bigInt == null) { + throw new NullPointerException("encodeInteger called with null parameter"); + } + return encodeBase64(toIntegerBytes(bigInt), false); + } + + /** + * Returns a byte-array representation of a BigInteger without sign bit. + * + * @param bigInt + * BigInteger to be converted + * @return a byte array representation of the BigInteger parameter + */ + static byte[] toIntegerBytes(final BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[bitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + /** + * Returns whether or not the octet is in the Base64 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the Base64 alphabet false otherwise. + */ + @Override + protected boolean isInAlphabet(final byte octet) { + return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1; + } + +} diff --git a/app/src/main/java/biweekly/util/org/apache/commons/codec/binary/BaseNCodec.java b/app/src/main/java/biweekly/util/org/apache/commons/codec/binary/BaseNCodec.java new file mode 100644 index 0000000000..da4e2b6108 --- /dev/null +++ b/app/src/main/java/biweekly/util/org/apache/commons/codec/binary/BaseNCodec.java @@ -0,0 +1,593 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + Copyright (c) 2013-2023, Michael Angstadt + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package biweekly.util.org.apache.commons.codec.binary; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import biweekly.util.org.apache.commons.codec.DecoderException; +import biweekly.util.org.apache.commons.codec.EncoderException; + +/** + * Abstract superclass for Base-N encoders and decoders. + * + *

+ * This class is thread-safe. + *

+ *

+ * biweekly integration note: Minor modifications were made to this class so that it could be + * incorporated into the biweekly code base. Defining the Apache Commons Codec library as a project dependency causes + * an issue with Android devices, which is why parts of its source code have been directly incorporated into the + * biweekly code base. + *

+ * + * @version $Id: BaseNCodec.java 1634404 2014-10-26 23:06:10Z ggregory $ + */ +public abstract class BaseNCodec { + + /** + * Holds thread context so classes can be thread-safe. + * + * This class is not itself thread-safe; each thread must allocate its own copy. + * + * @since 1.7 + */ + static class Context { + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + int ibitWorkArea; + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + long lbitWorkArea; + + /** + * Buffer for streaming. + */ + byte[] buffer; + + /** + * Position where next character should be written in the buffer. + */ + int pos; + + /** + * Position where next character should be read from the buffer. + */ + int readPos; + + /** + * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless, + * and must be thrown away. + */ + boolean eof; + + /** + * Variable tracks how many characters have been written to the current line. Only used when encoding. We use + * it to make sure each encoded line never goes beyond lineLength (if lineLength > 0). + */ + int currentLinePos; + + /** + * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This + * variable helps track that. + */ + int modulus; + + Context() { + } + + /** + * Returns a String useful for debugging (especially within a debugger.) + * + * @return a String useful for debugging. + */ + @SuppressWarnings("boxing") // OK to ignore boxing here + @Override + public String toString() { + return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, " + + "modulus=%s, pos=%s, readPos=%s]", this.getClass().getSimpleName(), Arrays.toString(buffer), + currentLinePos, eof, ibitWorkArea, lbitWorkArea, modulus, pos, readPos); + } + } + + /** + * EOF + * + * @since 1.7 + */ + static final int EOF = -1; + + /** + * MIME chunk size per RFC 2045 section 6.8. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 2045 section 6.8 + */ + public static final int MIME_CHUNK_SIZE = 76; + + /** + * PEM chunk size per RFC 1421 section 4.3.2.4. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 1421 section 4.3.2.4 + */ + public static final int PEM_CHUNK_SIZE = 64; + + private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; + + /** + * Defines the default buffer size - currently {@value} + * - must be large enough for at least one encoded block+separator + */ + private static final int DEFAULT_BUFFER_SIZE = 8192; + + /** Mask used to extract 8 bits, used in decoding bytes */ + protected static final int MASK_8BITS = 0xff; + + /** + * Byte used to pad output. + */ + protected static final byte PAD_DEFAULT = '='; // Allow static access to default + + /** + * @deprecated Use {@link #pad}. Will be removed in 2.0. + */ + @Deprecated + protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to vary later + + protected final byte pad; // instance variable just in case it needs to vary later + + /** Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32 */ + private final int unencodedBlockSize; + + /** Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32 */ + private final int encodedBlockSize; + + /** + * Chunksize for encoding. Not used when decoding. + * A value of zero or less implies no chunking of the encoded data. + * Rounded down to nearest multiple of encodedBlockSize. + */ + protected final int lineLength; + + /** + * Size of chunk separator. Not used unless {@link #lineLength} > 0. + */ + private final int chunkSeparatorLength; + + /** + * Note lineLength is rounded down to the nearest multiple of {@link #encodedBlockSize} + * If chunkSeparatorLength is zero, then chunking is disabled. + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length lineLength + * @param chunkSeparatorLength the chunk separator length, if relevant + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength) { + this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT); + } + + /** + * Note lineLength is rounded down to the nearest multiple of {@link #encodedBlockSize} + * If chunkSeparatorLength is zero, then chunking is disabled. + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length lineLength + * @param chunkSeparatorLength the chunk separator length, if relevant + * @param pad byte used as padding byte. + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength, final byte pad) { + this.unencodedBlockSize = unencodedBlockSize; + this.encodedBlockSize = encodedBlockSize; + final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0; + this.lineLength = useChunking ? (lineLength / encodedBlockSize) * encodedBlockSize : 0; + this.chunkSeparatorLength = chunkSeparatorLength; + + this.pad = pad; + } + + /** + * Returns true if this object has buffered data for reading. + * + * @param context the context to be used + * @return true if there is data still available for reading. + */ + boolean hasData(final Context context) { // package protected for access from I/O streams + return context.buffer != null; + } + + /** + * Returns the amount of buffered data available for reading. + * + * @param context the context to be used + * @return The amount of buffered data available for reading. + */ + int available(final Context context) { // package protected for access from I/O streams + return context.buffer != null ? context.pos - context.readPos : 0; + } + + /** + * Get the default buffer size. Can be overridden. + * + * @return {@link #DEFAULT_BUFFER_SIZE} + */ + protected int getDefaultBufferSize() { + return DEFAULT_BUFFER_SIZE; + } + + /** + * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}. + * @param context the context to be used + */ + private byte[] resizeBuffer(final Context context) { + if (context.buffer == null) { + context.buffer = new byte[getDefaultBufferSize()]; + context.pos = 0; + context.readPos = 0; + } else { + final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR]; + System.arraycopy(context.buffer, 0, b, 0, context.buffer.length); + context.buffer = b; + } + return context.buffer; + } + + /** + * Ensure that the buffer has room for size bytes + * + * @param size minimum spare space required + * @param context the context to be used + * @return the buffer + */ + protected byte[] ensureBufferSize(final int size, final Context context){ + if ((context.buffer == null) || (context.buffer.length < context.pos + size)){ + return resizeBuffer(context); + } + return context.buffer; + } + + /** + * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail + * bytes. Returns how many bytes were actually extracted. + *

+ * Package protected for access from I/O streams. + * + * @param b + * byte[] array to extract the buffered data into. + * @param bPos + * position in byte[] array to start extraction at. + * @param bAvail + * amount of bytes we're allowed to extract. We may extract fewer (if fewer are available). + * @param context + * the context to be used + * @return The number of bytes successfully extracted into the provided byte[] array. + */ + int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) { + if (context.buffer != null) { + final int len = Math.min(available(context), bAvail); + System.arraycopy(context.buffer, context.readPos, b, bPos, len); + context.readPos += len; + if (context.readPos >= context.pos) { + context.buffer = null; // so hasData() will return false, and this method can return -1 + } + return len; + } + return context.eof ? EOF : 0; + } + + /** + * Checks if a byte value is whitespace or not. + * Whitespace is taken to mean: space, tab, CR, LF + * @param byteToCheck + * the byte to check + * @return true if byte is whitespace, false otherwise + */ + protected static boolean isWhiteSpace(final byte byteToCheck) { + switch (byteToCheck) { + case ' ' : + case '\n' : + case '\r' : + case '\t' : + return true; + default : + return false; + } + } + + /** + * Encodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of + * the Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[]. + * + * @param obj + * Object to encode + * @return An object (of type byte[]) containing the Base-N encoded data which corresponds to the byte[] supplied. + * @throws EncoderException + * if the parameter supplied is not of type byte[] + */ + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof byte[])) { + throw new EncoderException("Parameter supplied to Base-N encode is not a byte[]"); + } + return encode((byte[]) obj); + } + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the Base-N alphabet. + * Uses UTF8 encoding. + * + * @param pArray + * a byte array containing binary data + * @return A String containing only Base-N character data + */ + public String encodeToString(final byte[] pArray) { + return newStringUtf8(encode(pArray)); + } + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet. + * Uses UTF8 encoding. + * + * @param pArray a byte array containing binary data + * @return String containing only character data in the appropriate alphabet. + */ + public String encodeAsString(final byte[] pArray){ + return newStringUtf8(encode(pArray)); + } + + /** + * Decodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of + * the Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[] or String. + * + * @param obj + * Object to decode + * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] or String + * supplied. + * @throws DecoderException + * if the parameter supplied is not of type byte[] + */ + public Object decode(final Object obj) throws DecoderException { + if (obj instanceof byte[]) { + return decode((byte[]) obj); + } else if (obj instanceof String) { + return decode((String) obj); + } else { + throw new DecoderException("Parameter supplied to Base-N decode is not a byte[] or a String"); + } + } + + /** + * Decodes a String containing characters in the Base-N alphabet. + * + * @param pArray + * A String containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] decode(final String pArray) { + return decode(getBytesUtf8(pArray)); + } + + /** + * Decodes a byte[] containing characters in the Base-N alphabet. + * + * @param pArray + * A byte array containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] decode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + decode(pArray, 0, pArray.length, context); + decode(pArray, 0, EOF, context); // Notify decoder of EOF. + final byte[] result = new byte[context.pos]; + readResults(result, 0, result.length, context); + return result; + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet. + * + * @param pArray + * a byte array containing binary data + * @return A byte array containing only the basen alphabetic character data + */ + public byte[] encode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + encode(pArray, 0, pArray.length, context); + encode(pArray, 0, EOF, context); // Notify encoder of EOF. + final byte[] buf = new byte[context.pos - context.readPos]; + readResults(buf, 0, buf.length, context); + return buf; + } + + // package protected for access from I/O streams + abstract void encode(byte[] pArray, int i, int length, Context context); + + // package protected for access from I/O streams + abstract void decode(byte[] pArray, int i, int length, Context context); + + /** + * Returns whether or not the octet is in the current alphabet. + * Does not allow whitespace or pad. + * + * @param value The value to test + * + * @return true if the value is defined in the current alphabet, false otherwise. + */ + protected abstract boolean isInAlphabet(byte value); + + /** + * Tests a given byte array to see if it contains only valid characters within the alphabet. + * The method optionally treats whitespace and pad as valid. + * + * @param arrayOctet byte array to test + * @param allowWSPad if true, then whitespace and PAD are also allowed + * + * @return true if all bytes are valid characters in the alphabet or if the byte array is empty; + * false, otherwise + */ + public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isInAlphabet(arrayOctet[i]) && + (!allowWSPad || (arrayOctet[i] != pad) && !isWhiteSpace(arrayOctet[i]))) { + return false; + } + } + return true; + } + + /** + * Tests a given String to see if it contains only valid characters within the alphabet. + * The method treats whitespace and PAD as valid. + * + * @param basen String to test + * @return true if all characters in the String are valid characters in the alphabet or if + * the String is empty; false, otherwise + * @see #isInAlphabet(byte[], boolean) + */ + public boolean isInAlphabet(final String basen) { + return isInAlphabet(getBytesUtf8(basen), true); + } + + /** + * Tests a given byte array to see if it contains any characters within the alphabet or PAD. + * + * Intended for use in checking line-ending arrays + * + * @param arrayOctet + * byte array to test + * @return true if any byte is a valid character in the alphabet or PAD; false otherwise + */ + protected boolean containsAlphabetOrPad(final byte[] arrayOctet) { + if (arrayOctet == null) { + return false; + } + for (final byte element : arrayOctet) { + if (pad == element || isInAlphabet(element)) { + return true; + } + } + return false; + } + + /** + * Calculates the amount of space needed to encode the supplied array. + * + * @param pArray byte[] array which will later be encoded + * + * @return amount of space needed to encoded the supplied array. + * Returns a long since a max-len array will require > Integer.MAX_VALUE + */ + public long getEncodedLength(final byte[] pArray) { + // Calculate non-chunked size - rounded up to allow for padding + // cast to long is needed to avoid possibility of overflow + long len = ((pArray.length + unencodedBlockSize-1) / unencodedBlockSize) * (long) encodedBlockSize; + if (lineLength > 0) { // We're using chunking + // Round up to nearest multiple + len += ((len + lineLength-1) / lineLength) * chunkSeparatorLength; + } + return len; + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + */ + protected static byte[] getBytesUtf8(final String string) { + if (string == null) { + return null; + } + + try { + return string.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + //should never be thrown because all JVMs must support UTF-8 + throw new RuntimeException(e); + } + } + + /** + * Constructs a new String by decoding the specified array of bytes using the UTF-8 charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new String decoded from the specified array of bytes using the UTF-8 charset, + * or null if the input byte array was null. + */ + protected static String newStringUtf8(final byte[] bytes) { + if (bytes == null) { + return null; + } + + try { + return new String(bytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + //should never be thrown because all JVMs must support UTF-8 + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/biweekly/util/package-info.java b/app/src/main/java/biweekly/util/package-info.java new file mode 100644 index 0000000000..ecc8d86b78 --- /dev/null +++ b/app/src/main/java/biweekly/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains miscellaneous utility classes. + */ +package biweekly.util; \ No newline at end of file