001package biweekly.component;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import biweekly.ICalDataType;
007import biweekly.ICalVersion;
008import biweekly.ICalendar;
009import biweekly.ValidationWarnings.WarningsGroup;
010import biweekly.Warning;
011import biweekly.property.ICalProperty;
012import biweekly.property.RawProperty;
013import biweekly.property.Status;
014import biweekly.util.ListMultimap;
015
016/*
017 Copyright (c) 2013-2015, 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
041/**
042 * Base class for all iCalendar components.
043 * @author Michael Angstadt
044 */
045public abstract class ICalComponent {
046        protected final ListMultimap<Class<? extends ICalComponent>, ICalComponent> components = new ListMultimap<Class<? extends ICalComponent>, ICalComponent>();
047        protected final ListMultimap<Class<? extends ICalProperty>, ICalProperty> properties = new ListMultimap<Class<? extends ICalProperty>, ICalProperty>();
048
049        /**
050         * Gets the first property of a given class.
051         * @param clazz the property class
052         * @return the property or null if not found
053         */
054        public <T extends ICalProperty> T getProperty(Class<T> clazz) {
055                return clazz.cast(properties.first(clazz));
056        }
057
058        /**
059         * Gets all properties of a given class.
060         * @param clazz the property class
061         * @return the properties
062         */
063        public <T extends ICalProperty> List<T> getProperties(Class<T> clazz) {
064                List<ICalProperty> props = properties.get(clazz);
065
066                //cast to the requested class
067                List<T> ret = new ArrayList<T>(props.size());
068                for (ICalProperty property : props) {
069                        ret.add(clazz.cast(property));
070                }
071                return ret;
072        }
073
074        /**
075         * Gets all the properties associated with this component.
076         * @return the properties
077         */
078        public ListMultimap<Class<? extends ICalProperty>, ICalProperty> getProperties() {
079                return properties;
080        }
081
082        /**
083         * Adds a property to this component.
084         * @param property the property to add
085         */
086        public void addProperty(ICalProperty property) {
087                properties.put(property.getClass(), property);
088        }
089
090        /**
091         * Replaces all existing properties of the given class with a single
092         * property instance.
093         * @param property the property (must not be null)
094         */
095        public void setProperty(ICalProperty property) {
096                properties.replace(property.getClass(), property);
097        }
098
099        /**
100         * Replaces all existing properties of the given class with a single
101         * property instance. If the property instance is null, then all instances
102         * of that property will be removed.
103         * @param clazz the property class (e.g. "DateStart.class")
104         * @param property the property or null to remove
105         */
106        public <T extends ICalProperty> void setProperty(Class<T> clazz, T property) {
107                properties.replace(clazz, property);
108        }
109
110        /**
111         * Removes properties from the iCalendar object.
112         * @param clazz the class of the properties to remove (e.g.
113         * "DateStart.class")
114         */
115        public void removeProperties(Class<? extends ICalProperty> clazz) {
116                properties.removeAll(clazz);
117        }
118
119        /**
120         * Removes components from the iCalendar object.
121         * @param clazz the class of the components to remove (e.g. "VEvent.class")
122         */
123        public void removeComponents(Class<? extends ICalComponent> clazz) {
124                components.removeAll(clazz);
125        }
126
127        /**
128         * Gets the first experimental property with a given name.
129         * @param name the property name (e.g. "X-ALT-DESC")
130         * @return the property or null if none were found
131         */
132        public RawProperty getExperimentalProperty(String name) {
133                for (RawProperty raw : getProperties(RawProperty.class)) {
134                        if (raw.getName().equalsIgnoreCase(name)) {
135                                return raw;
136                        }
137                }
138                return null;
139        }
140
141        /**
142         * Gets all experimental properties with a given name.
143         * @param name the property name (e.g. "X-ALT-DESC")
144         * @return the properties
145         */
146        public List<RawProperty> getExperimentalProperties(String name) {
147                List<RawProperty> props = new ArrayList<RawProperty>();
148
149                for (RawProperty raw : getProperties(RawProperty.class)) {
150                        if (raw.getName().equalsIgnoreCase(name)) {
151                                props.add(raw);
152                        }
153                }
154
155                return props;
156        }
157
158        /**
159         * Gets all experimental properties associated with this component.
160         * @return the properties
161         */
162        public List<RawProperty> getExperimentalProperties() {
163                return getProperties(RawProperty.class);
164        }
165
166        /**
167         * Adds an experimental property to this component.
168         * @param name the property name (e.g. "X-ALT-DESC")
169         * @param value the property value
170         * @return the property object that was created
171         */
172        public RawProperty addExperimentalProperty(String name, String value) {
173                return addExperimentalProperty(name, null, value);
174        }
175
176        /**
177         * Adds an experimental property to this component.
178         * @param name the property name (e.g. "X-ALT-DESC")
179         * @param dataType the property's data type (e.g. "text") or null if unknown
180         * @param value the property value
181         * @return the property object that was created
182         */
183        public RawProperty addExperimentalProperty(String name, ICalDataType dataType, String value) {
184                RawProperty raw = new RawProperty(name, dataType, value);
185                addProperty(raw);
186                return raw;
187        }
188
189        /**
190         * Adds an experimental property to this component, removing all existing
191         * properties that have the same name.
192         * @param name the property name (e.g. "X-ALT-DESC")
193         * @param value the property value
194         * @return the property object that was created
195         */
196        public RawProperty setExperimentalProperty(String name, String value) {
197                return setExperimentalProperty(name, null, value);
198        }
199
200        /**
201         * Adds an experimental property to this component, removing all existing
202         * properties that have the same name.
203         * @param name the property name (e.g. "X-ALT-DESC")
204         * @param dataType the property's data type (e.g. "text") or null if unknown
205         * @param value the property value
206         * @return the property object that was created
207         */
208        public RawProperty setExperimentalProperty(String name, ICalDataType dataType, String value) {
209                removeExperimentalProperty(name);
210                return addExperimentalProperty(name, dataType, value);
211        }
212
213        /**
214         * Removes all experimental properties that have the given name.
215         * @param name the component name (e.g. "X-ALT-DESC")
216         */
217        public void removeExperimentalProperty(String name) {
218                List<RawProperty> xproperties = getExperimentalProperties(name);
219                for (RawProperty xproperty : xproperties) {
220                        properties.remove(xproperty.getClass(), xproperty);
221                }
222        }
223
224        /**
225         * Gets the first component of a given class.
226         * @param clazz the component class
227         * @return the component or null if not found
228         */
229        public <T extends ICalComponent> T getComponent(Class<T> clazz) {
230                return clazz.cast(components.first(clazz));
231        }
232
233        /**
234         * Gets all components of a given class.
235         * @param clazz the component class
236         * @return the components
237         */
238        public <T extends ICalComponent> List<T> getComponents(Class<T> clazz) {
239                List<ICalComponent> comp = components.get(clazz);
240
241                //cast to the requested class
242                List<T> ret = new ArrayList<T>(comp.size());
243                for (ICalComponent property : comp) {
244                        ret.add(clazz.cast(property));
245                }
246                return ret;
247        }
248
249        /**
250         * Gets all the sub-components associated with this component.
251         * @return the sub-components
252         */
253        public ListMultimap<Class<? extends ICalComponent>, ICalComponent> getComponents() {
254                return components;
255        }
256
257        /**
258         * Adds a sub-component to this component.
259         * @param component the component to add
260         */
261        public void addComponent(ICalComponent component) {
262                components.put(component.getClass(), component);
263        }
264
265        /**
266         * Replaces all components of a given class with the given component.
267         * @param component the component (must not be null)
268         */
269        public void setComponent(ICalComponent component) {
270                components.replace(component.getClass(), component);
271        }
272
273        /**
274         * Replaces all components of a given class with the given component. If the
275         * component instance is null, then all instances of that component will be
276         * removed.
277         * @param clazz the component's class
278         * @param component the component or null to remove
279         */
280        public <T extends ICalComponent> void setComponent(Class<T> clazz, T component) {
281                components.replace(clazz, component);
282        }
283
284        /**
285         * Gets the first experimental sub-component with a given name.
286         * @param name the component name (e.g. "X-PARTY")
287         * @return the component or null if none were found
288         */
289        public RawComponent getExperimentalComponent(String name) {
290                for (RawComponent raw : getComponents(RawComponent.class)) {
291                        if (raw.getName().equalsIgnoreCase(name)) {
292                                return raw;
293                        }
294                }
295                return null;
296        }
297
298        /**
299         * Gets all experimental sub-component with a given name.
300         * @param name the component name (e.g. "X-PARTY")
301         * @return the components
302         */
303        public List<RawComponent> getExperimentalComponents(String name) {
304                List<RawComponent> props = new ArrayList<RawComponent>();
305
306                for (RawComponent raw : getComponents(RawComponent.class)) {
307                        if (raw.getName().equalsIgnoreCase(name)) {
308                                props.add(raw);
309                        }
310                }
311
312                return props;
313        }
314
315        /**
316         * Gets all experimental sub-components associated with this component.
317         * @return the sub-components
318         */
319        public List<RawComponent> getExperimentalComponents() {
320                return getComponents(RawComponent.class);
321        }
322
323        /**
324         * Adds an experimental sub-component to this component.
325         * @param name the component name (e.g. "X-PARTY")
326         * @return the component object that was created
327         */
328        public RawComponent addExperimentalComponent(String name) {
329                RawComponent raw = new RawComponent(name);
330                addComponent(raw);
331                return raw;
332        }
333
334        /**
335         * Adds an experimental sub-component to this component, removing all
336         * existing components that have the same name.
337         * @param name the component name (e.g. "X-PARTY")
338         * @return the component object that was created
339         */
340        public RawComponent setExperimentalComponents(String name) {
341                removeExperimentalComponents(name);
342                return addExperimentalComponent(name);
343        }
344
345        /**
346         * Removes all experimental sub-components that have the given name.
347         * @param name the component name (e.g. "X-PARTY")
348         */
349        public void removeExperimentalComponents(String name) {
350                List<RawComponent> xcomponents = getExperimentalComponents(name);
351                for (RawComponent xcomponent : xcomponents) {
352                        components.remove(xcomponent.getClass(), xcomponent);
353                }
354        }
355
356        /**
357         * Checks the component for data consistency problems or deviations from the
358         * spec. These problems will not prevent the component from being written to
359         * a data stream, but may prevent it from being parsed correctly by the
360         * consuming application. These problems can largely be avoided by reading
361         * the Javadocs of the component class, or by being familiar with the
362         * iCalendar standard.
363         * @param hierarchy the hierarchy of components that the component belongs
364         * to
365         * @param version the version to validate against
366         * @see ICalendar#validate
367         * @return a list of warnings or an empty list if no problems were found
368         */
369        public final List<WarningsGroup> validate(List<ICalComponent> hierarchy, ICalVersion version) {
370                List<WarningsGroup> warnings = new ArrayList<WarningsGroup>();
371
372                //validate this component
373                List<Warning> warningsBuf = new ArrayList<Warning>(0);
374                validate(hierarchy, version, warningsBuf);
375                if (!warningsBuf.isEmpty()) {
376                        warnings.add(new WarningsGroup(this, hierarchy, warningsBuf));
377                }
378
379                //add this component to the hierarchy list
380                //copy the list so other validate() calls aren't effected
381                hierarchy = new ArrayList<ICalComponent>(hierarchy);
382                hierarchy.add(this);
383
384                //validate properties
385                for (ICalProperty property : properties.values()) {
386                        List<Warning> propWarnings = property.validate(hierarchy, version);
387                        if (!propWarnings.isEmpty()) {
388                                warnings.add(new WarningsGroup(property, hierarchy, propWarnings));
389                        }
390                }
391
392                //validate sub-components
393                for (ICalComponent component : components.values()) {
394                        warnings.addAll(component.validate(hierarchy, version));
395                }
396
397                return warnings;
398        }
399
400        /**
401         * <p>
402         * Checks the component for data consistency problems or deviations from the
403         * spec.
404         * </p>
405         * <p>
406         * This method should be overridden by child classes that wish to provide
407         * validation logic. The default implementation of this method does nothing.
408         * </p>
409         * @param components the hierarchy of components that the component belongs
410         * to
411         * @param version the version to validate against
412         * @param warnings the list to add the warnings to
413         */
414        protected void validate(List<ICalComponent> components, ICalVersion version, List<Warning> warnings) {
415                //do nothing
416        }
417
418        /**
419         * Utility method for validating that there is exactly one instance of each
420         * of the given properties.
421         * @param warnings the list to add the warnings to
422         * @param classes the properties to check
423         */
424        protected void checkRequiredCardinality(List<Warning> warnings, Class<? extends ICalProperty>... classes) {
425                for (Class<? extends ICalProperty> clazz : classes) {
426                        List<? extends ICalProperty> props = getProperties(clazz);
427
428                        if (props.isEmpty()) {
429                                warnings.add(Warning.validate(2, clazz.getSimpleName()));
430                                continue;
431                        }
432
433                        if (props.size() > 1) {
434                                warnings.add(Warning.validate(3, clazz.getSimpleName()));
435                                continue;
436                        }
437                }
438        }
439
440        /**
441         * Utility method for validating that there is no more than one instance of
442         * each of the given properties.
443         * @param warnings the list to add the warnings to
444         * @param classes the properties to check
445         */
446        protected void checkOptionalCardinality(List<Warning> warnings, Class<? extends ICalProperty>... classes) {
447                for (Class<? extends ICalProperty> clazz : classes) {
448                        List<? extends ICalProperty> props = getProperties(clazz);
449
450                        if (props.size() > 1) {
451                                warnings.add(Warning.validate(3, clazz.getSimpleName()));
452                                continue;
453                        }
454                }
455        }
456
457        /**
458         * Utility method for validating the {@link Status} property of a component.
459         * @param warnings the list to add the warnings to
460         * @param allowed the valid statuses
461         */
462        protected void checkStatus(List<Warning> warnings, Status... allowed) {
463                Status actual = getProperty(Status.class);
464                if (actual == null) {
465                        return;
466                }
467
468                List<String> allowedValues = new ArrayList<String>(allowed.length);
469                for (Status status : allowed) {
470                        String value = status.getValue().toLowerCase();
471                        allowedValues.add(value);
472                }
473
474                String actualValue = actual.getValue().toLowerCase();
475                if (!allowedValues.contains(actualValue)) {
476                        warnings.add(Warning.validate(13, actual.getValue(), allowedValues));
477                }
478        }
479}