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