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