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