mirror of https://github.com/M66B/FairEmail.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
818 lines
29 KiB
818 lines
29 KiB
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.XMLConstants;
|
|
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
|
|
/**
|
|
* <p>
|
|
* Represents an XML document that contains iCalendar objects ("xCal" standard).
|
|
* This class can be used to read and write xCal documents.
|
|
* </p>
|
|
* <p>
|
|
* <b>Examples:</b>
|
|
* </p>
|
|
*
|
|
* <pre class="brush:java">
|
|
* 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);
|
|
* </pre>
|
|
* @author Michael Angstadt
|
|
* @see <a href="http://tools.ietf.org/html/rfc6321">RFC 6321</a>
|
|
*/
|
|
//@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));
|
|
}
|
|
|
|
/**
|
|
* <p>
|
|
* Parses an xCal document from a reader.
|
|
* </p>
|
|
* <p>
|
|
* 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.
|
|
* </p>
|
|
* @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 <icalendar> 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<ICalendar> 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 <a href=
|
|
* "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
|
|
* >xalan</a> 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<String, String> 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 <a href=
|
|
* "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
|
|
* >xalan</a> 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<String, String> 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 <a href=
|
|
* "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
|
|
* >xalan</a> 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<String, String> 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 <a href=
|
|
* "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
|
|
* >xalan</a> 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<String, String> outputProperties) throws TransformerException {
|
|
Transformer transformer;
|
|
try {
|
|
TransformerFactory factory = TransformerFactory.newInstance();
|
|
factory.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
|
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
|
transformer = factory.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<String, String> 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<Element> 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 <vcalendar> 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<? extends ICalComponent> 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 <properties> 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 <components> 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<? extends ICalProperty> 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 <parameters> element, but parse them all incase there are more
|
|
List<Element> paramElements = XmlUtils.toElementList(parametersElement.getChildNodes());
|
|
for (Element paramElement : paramElements) {
|
|
if (!XCAL_NS.equals(paramElement.getNamespaceURI())) {
|
|
continue;
|
|
}
|
|
|
|
String name = paramElement.getLocalName().toUpperCase();
|
|
List<Element> 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<Element> getVCalendarElements() {
|
|
return (icalendarRootElement == null) ? Collections.<Element> emptyList() : getChildElements(icalendarRootElement, VCALENDAR);
|
|
}
|
|
|
|
private List<Element> getChildElements(Element parent, QName qname) {
|
|
List<Element> elements = new ArrayList<Element>();
|
|
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<VTimezone> 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<String, List<String>> 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
|
|
}
|
|
}
|
|
}
|