001package ezvcard.io.xml;
002
003import java.util.Arrays;
004import java.util.Collection;
005import java.util.Collections;
006import java.util.List;
007import java.util.stream.Collectors;
008
009import org.w3c.dom.Document;
010import org.w3c.dom.Element;
011
012import ezvcard.VCardDataType;
013import ezvcard.VCardVersion;
014import ezvcard.util.XmlUtils;
015
016/*
017 Copyright (c) 2012-2026, Michael Angstadt
018 All rights reserved.
019
020 Redistribution and use in source and binary forms, with or without
021 modification, are permitted provided that the following conditions are met: 
022
023 1. Redistributions of source code must retain the above copyright notice, this
024 list of conditions and the following disclaimer. 
025 2. Redistributions in binary form must reproduce the above copyright notice,
026 this list of conditions and the following disclaimer in the documentation
027 and/or other materials provided with the distribution. 
028
029 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
030 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
031 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
032 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
033 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
034 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
035 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
036 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
037 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
038 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
039
040 The views and conclusions contained in the software and documentation are those
041 of the authors and should not be interpreted as representing official policies, 
042 either expressed or implied, of the FreeBSD Project.
043 */
044
045/**
046 * Wraps xCard functionality around an XML {@link Element} object.
047 * @author Michael Angstadt
048 */
049public class XCardElement {
050        private final Document document;
051        private final Element element;
052        private final VCardVersion version;
053        private final String namespace;
054
055        /**
056         * Wraps an existing XML element.
057         * @param element the XML element
058         */
059        public XCardElement(Element element) {
060                this(element, VCardVersion.V4_0);
061        }
062
063        /**
064         * Wraps an existing XML element.
065         * @param element the XML element
066         * @param version the vCard version
067         */
068        public XCardElement(Element element, VCardVersion version) {
069                this.document = element.getOwnerDocument();
070                this.element = element;
071                this.version = version;
072                namespace = version.getXmlNamespace();
073        }
074
075        /**
076         * Gets the first value with one of the given data types.
077         * @param dataTypes the data type(s) to look for (null signifies the
078         * "unknown" data type)
079         * @return the value or null if not found
080         */
081        public String first(VCardDataType... dataTypes) {
082                //@formatter:off
083                String[] names = Arrays.stream(dataTypes)
084                        .map(XCardElement::toLocalName)
085                .toArray(len -> new String[len]);
086                //@formatter:on
087
088                return first(names);
089        }
090
091        /**
092         * Gets the value of the first child element with one of the given names.
093         * @param names the possible names of the element
094         * @return the element's text or null if not found
095         */
096        public String first(String... names) {
097                List<String> localNamesList = Arrays.asList(names);
098
099                //@formatter:off
100                return children().stream()
101                        .filter(child -> localNamesList.contains(child.getLocalName()))
102                        .filter(child -> namespace.equals(child.getNamespaceURI()))
103                        .map(Element::getTextContent)
104                .findFirst().orElse(null);
105                //@formatter:on
106        }
107
108        /**
109         * Gets all the values of a given data type.
110         * @param dataType the data type to look for
111         * @return the values
112         */
113        public List<String> all(VCardDataType dataType) {
114                String dataTypeStr = toLocalName(dataType);
115                return all(dataTypeStr);
116        }
117
118        /**
119         * Gets the value of all non-empty child elements that have the given name.
120         * @param localName the element name
121         * @return the values of the child elements
122         */
123        public List<String> all(String localName) {
124                //@formatter:off
125                return children().stream()
126                        .filter(child -> localName.equals(child.getLocalName()))
127                        .filter(child -> namespace.equals(child.getNamespaceURI()))
128                        .map(Element::getTextContent)
129                        .filter(text -> !text.isEmpty())
130                .collect(Collectors.toList());
131                //@formatter:on
132        }
133
134        /**
135         * Adds a value.
136         * @param dataType the data type or null for the "unknown" data type
137         * @param value the value
138         * @return the created element
139         */
140        public Element append(VCardDataType dataType, String value) {
141                String dataTypeStr = toLocalName(dataType);
142                return append(dataTypeStr, value);
143        }
144
145        /**
146         * Adds a child element.
147         * @param name the name of the child element
148         * @param value the value of the child element.
149         * @return the created element
150         */
151        public Element append(String name, String value) {
152                Element child = document.createElementNS(namespace, name);
153                child.setTextContent(value);
154                element.appendChild(child);
155                return child;
156        }
157
158        /**
159         * Adds multiple child elements, each with the same name.
160         * @param name the name for all the child elements
161         * @param values the values of each child element
162         * @return the created elements
163         */
164        public List<Element> append(String name, Collection<String> values) {
165                if (values.isEmpty()) {
166                        Element element = append(name, (String) null);
167                        return Collections.singletonList(element);
168                }
169
170                //@formatter:off
171                return values.stream()
172                        .map(value -> append(name, value))
173                .collect(Collectors.toList());
174                //@formatter:on
175        }
176
177        /**
178         * Gets the owner document.
179         * @return the owner document
180         */
181        public Document document() {
182                return document;
183        }
184
185        /**
186         * Gets the wrapped XML element.
187         * @return the wrapped XML element
188         */
189        public Element element() {
190                return element;
191        }
192
193        /**
194         * Gets the vCard version.
195         * @return the vCard version
196         */
197        public VCardVersion version() {
198                return version;
199        }
200
201        /**
202         * Gets the child elements of the XML element.
203         * @return the child elements
204         */
205        private List<Element> children() {
206                return XmlUtils.toElementList(element.getChildNodes());
207        }
208
209        /**
210         * Finds the first child element that has the xCard namespace and returns
211         * its data type and value. If no such element is found, the parent
212         * {@link XCardElement}'s text content, along with a null data type, is
213         * returned.
214         * @return the value and data type
215         */
216        public XCardValue firstValue() {
217                String elementNamespace = version.getXmlNamespace();
218
219                //@formatter:off
220                return children().stream()
221                        .filter(child -> elementNamespace.equals(child.getNamespaceURI()))
222                        .map(child -> {
223                                VCardDataType dataType = toDataType(child.getLocalName());
224                                String value = child.getTextContent();
225                                return new XCardValue(dataType, value);
226                        })
227                .findFirst().orElseGet(() -> new XCardValue(null, element.getTextContent()));
228                //@formatter:on
229        }
230
231        /**
232         * Gets the appropriate XML local name of a {@link VCardDataType} object.
233         * @param dataType the data type or null for "unknown"
234         * @return the local name (e.g. "text")
235         */
236        private static String toLocalName(VCardDataType dataType) {
237                return (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
238        }
239
240        /**
241         * Converts an XML local name to the appropriate {@link VCardDataType}
242         * object.
243         * @param localName the local name (e.g. "text")
244         * @return the data type or null for "unknown"
245         */
246        private static VCardDataType toDataType(String localName) {
247                return "unknown".equals(localName) ? null : VCardDataType.get(localName);
248        }
249
250        /**
251         * Represents the data type and value of a child element under an
252         * {@link XCardElement}.
253         */
254        public static class XCardValue {
255                private final VCardDataType dataType;
256                private final String value;
257
258                /**
259                 * @param dataType the data type or null if "unknown"
260                 * @param value the value
261                 */
262                public XCardValue(VCardDataType dataType, String value) {
263                        this.dataType = dataType;
264                        this.value = value;
265                }
266
267                /**
268                 * Gets the data type
269                 * @return the data type or null if "unknown"
270                 */
271                public VCardDataType getDataType() {
272                        return dataType;
273                }
274
275                /**
276                 * Get the value.
277                 * @return the value
278                 */
279                public String getValue() {
280                        return value;
281                }
282        }
283}