001package ezvcard.io.json;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Collections;
006import java.util.List;
007import java.util.Objects;
008import java.util.stream.Collectors;
009
010import ezvcard.property.Categories;
011import ezvcard.property.Note;
012import ezvcard.property.StructuredName;
013
014/*
015 Copyright (c) 2012-2026, Michael Angstadt
016 All rights reserved.
017
018 Redistribution and use in source and binary forms, with or without
019 modification, are permitted provided that the following conditions are met: 
020
021 1. Redistributions of source code must retain the above copyright notice, this
022 list of conditions and the following disclaimer. 
023 2. Redistributions in binary form must reproduce the above copyright notice,
024 this list of conditions and the following disclaimer in the documentation
025 and/or other materials provided with the distribution. 
026
027 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
028 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
029 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
030 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
031 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
032 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
033 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
034 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
035 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
036 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037
038 The views and conclusions contained in the software and documentation are those
039 of the authors and should not be interpreted as representing official policies, 
040 either expressed or implied, of the FreeBSD Project.
041 */
042
043/**
044 * Holds the data type and value of a jCard property.
045 * @author Michael Angstadt
046 */
047public class JCardValue {
048        private final List<JsonValue> values;
049
050        /**
051         * Creates a new jCard value.
052         * @param values the values
053         */
054        public JCardValue(List<JsonValue> values) {
055                this.values = Collections.unmodifiableList(values);
056        }
057
058        /**
059         * Creates a new jCard value.
060         * @param values the values
061         */
062        public JCardValue(JsonValue... values) {
063                this.values = Arrays.asList(values); //unmodifiable
064        }
065
066        /**
067         * Creates a single-valued value.
068         * @param value the value
069         * @return the jCard value
070         */
071        public static JCardValue single(Object value) {
072                return new JCardValue(new JsonValue(value));
073        }
074
075        /**
076         * Creates a multi-valued value.
077         * @param values the values
078         * @return the jCard value
079         */
080        public static JCardValue multi(Object... values) {
081                return multi(Arrays.asList(values));
082        }
083
084        /**
085         * Creates a multi-valued value.
086         * @param values the values
087         * @return the jCard value
088         */
089        public static JCardValue multi(List<?> values) {
090                //@formatter:off
091                return new JCardValue(values.stream()
092                        .map(JsonValue::new)
093                .collect(Collectors.toList()));
094                //@formatter:on
095        }
096
097        /**
098         * <p>
099         * Creates a structured value.
100         * </p>
101         * <p>
102         * This method accepts a vararg of {@link Object} instances. {@link List}
103         * objects will be treated as multi-valued components. Null objects will be
104         * treated as empty components.
105         * </p>
106         * @param values the values
107         * @return the jCard value
108         */
109        public static JCardValue structured(Object... values) {
110                //@formatter:off
111                return structured(Arrays.stream(values)
112                        .map(value -> (value instanceof List) ? (List<?>) value : Collections.singletonList(value))
113                .collect(Collectors.toList()));
114                //@formatter:on
115        }
116
117        /**
118         * Creates a structured value.
119         * @param values the values
120         * @return the jCard value
121         */
122        public static JCardValue structured(List<List<?>> values) {
123                List<JsonValue> array = new ArrayList<>(values.size());
124
125                for (List<?> list : values) {
126                        if (list.isEmpty()) {
127                                array.add(new JsonValue(""));
128                                continue;
129                        }
130
131                        if (list.size() == 1) {
132                                Object value = list.get(0);
133                                if (value == null) {
134                                        value = "";
135                                }
136                                array.add(new JsonValue(value));
137                                continue;
138                        }
139
140                        //@formatter:off
141                        List<JsonValue> subArray = list.stream()
142                                .map(value -> (value == null) ? "" : value)
143                                .map(JsonValue::new)
144                        .collect(Collectors.toList());
145                        //@formatter:on
146
147                        array.add(new JsonValue(subArray));
148                }
149
150                return new JCardValue(new JsonValue(array));
151        }
152
153        /**
154         * Gets all the JSON values.
155         * @return the JSON values
156         */
157        public List<JsonValue> getValues() {
158                return values;
159        }
160
161        /**
162         * Gets the value of a single-valued property (such as {@link Note}).
163         * @return the value or empty string if not found
164         */
165        public String asSingle() {
166                if (values.isEmpty()) {
167                        return "";
168                }
169
170                JsonValue first = values.get(0);
171                if (first.isNull()) {
172                        return "";
173                }
174
175                Object obj = first.getValue();
176                if (obj != null) {
177                        return obj.toString();
178                }
179
180                //get the first element of the array
181                List<JsonValue> array = first.getArray();
182                if (array != null && !array.isEmpty()) {
183                        obj = array.get(0).getValue();
184                        if (obj != null) {
185                                return obj.toString();
186                        }
187                }
188
189                return "";
190        }
191
192        /**
193         * Gets the value of a structured property (such as {@link StructuredName}).
194         * @return the values or empty list if not found
195         */
196        public List<List<String>> asStructured() {
197                return new StructuredValueConverter().convert();
198        }
199
200        private class StructuredValueConverter {
201                public List<List<String>> convert() {
202                        if (values.isEmpty()) {
203                                return Collections.emptyList();
204                        }
205
206                        JsonValue first = values.get(0);
207
208                        //["gender", {}, "text", ["M", "text"] ]
209                        List<JsonValue> array = first.getArray();
210                        if (array != null) {
211                                return fromArray(array);
212                        }
213
214                        //get the first value if it's not enclosed in an array
215                        //["gender", {}, "text", "M"]
216                        Object obj = first.getValue();
217                        if (obj != null) {
218                                return Collections.singletonList(fromValue(obj));
219                        }
220
221                        //["gender", {}, "text", null]
222                        if (first.isNull()) {
223                                return Collections.singletonList(fromNull());
224                        }
225
226                        return Collections.emptyList();
227                }
228
229                private List<List<String>> fromArray(List<JsonValue> array) {
230                        //@formatter:off
231                        return array.stream()
232                                .map(this::fromArrayValue)
233                                .filter(v -> v != null)
234                        .collect(Collectors.toList());
235                        //@formatter:on
236                }
237
238                private List<String> fromArrayValue(JsonValue value) {
239                        if (value.isNull()) {
240                                return fromNull();
241                        }
242
243                        Object obj = value.getValue();
244                        if (obj != null) {
245                                return fromValue(obj);
246                        }
247
248                        List<JsonValue> subArray = value.getArray();
249                        if (subArray != null) {
250                                //@formatter:off
251                                List<String> component = subArray.stream()
252                                        .map(jsonValue -> jsonValue.isNull() ? "" : jsonValue.getValue())
253                                        .filter(o -> o != null)
254                                        .map(Object::toString)
255                                .collect(Collectors.toList());
256                                //@formatter:on
257
258                                if (component.size() == 1 && component.get(0).isEmpty()) {
259                                        return Collections.emptyList();
260                                }
261                                return component;
262                        }
263
264                        return null;
265                }
266
267                private List<String> fromNull() {
268                        return Collections.emptyList();
269                }
270
271                private List<String> fromValue(Object obj) {
272                        String s = obj.toString();
273                        return s.isEmpty() ? Collections.emptyList() : Collections.singletonList(s);
274                }
275        }
276
277        /**
278         * Gets the value of a multi-valued property (such as {@link Categories} ).
279         * @return the values or empty list if not found
280         */
281        public List<String> asMulti() {
282                if (values.isEmpty()) {
283                        return Collections.emptyList();
284                }
285
286                List<String> multi = new ArrayList<>(values.size());
287                for (JsonValue value : values) {
288                        if (value.isNull()) {
289                                multi.add("");
290                                continue;
291                        }
292
293                        Object obj = value.getValue();
294                        if (obj != null) {
295                                multi.add(obj.toString());
296                                continue;
297                        }
298                }
299                return multi;
300        }
301
302        @Override
303        public boolean equals(Object obj) {
304                if (this == obj) return true;
305                if (obj == null) return false;
306                if (getClass() != obj.getClass()) return false;
307                JCardValue other = (JCardValue) obj;
308                return Objects.equals(values, other.values);
309        }
310
311        @Override
312        public int hashCode() {
313                return Objects.hash(values);
314        }
315}