001    package biweekly.component;
002    
003    import java.util.ArrayList;
004    import java.util.List;
005    
006    import biweekly.ICalendar;
007    import biweekly.property.ICalProperty;
008    import biweekly.property.RawProperty;
009    import biweekly.util.ListMultimap;
010    import biweekly.util.StringUtils;
011    import biweekly.util.StringUtils.JoinCallback;
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            @SuppressWarnings("unchecked")
052            public <T extends ICalProperty> T getProperty(Class<T> clazz) {
053                    return (T) 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            @SuppressWarnings("unchecked")
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((T) 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
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                    RawProperty raw = new RawProperty(name, value); //TODO rename to XProperty
164                    addProperty(raw);
165                    return raw;
166            }
167    
168            /**
169             * Adds an experimental property to this component, removing all existing
170             * properties that have the same name.
171             * @param name the property name (e.g. "X-ALT-DESC")
172             * @param value the property value
173             * @return the property object that was created
174             */
175            public RawProperty setExperimentalProperty(String name, String value) {
176                    removeExperimentalProperty(name);
177                    RawProperty raw = new RawProperty(name, value);
178                    addProperty(raw);
179                    return raw;
180            }
181    
182            /**
183             * Removes all experimental properties that have the given name.
184             * @param name the component name (e.g. "X-ALT-DESC")
185             */
186            public void removeExperimentalProperty(String name) {
187                    List<RawProperty> xproperties = getExperimentalProperties(name);
188                    for (RawProperty xproperty : xproperties) {
189                            properties.remove(xproperty.getClass(), xproperty);
190                    }
191            }
192    
193            /**
194             * Gets the first component of a given class.
195             * @param clazz the component class
196             * @return the component or null if not found
197             */
198            @SuppressWarnings("unchecked")
199            public <T extends ICalComponent> T getComponent(Class<T> clazz) {
200                    return (T) components.first(clazz);
201            }
202    
203            /**
204             * Gets all components of a given class.
205             * @param clazz the component class
206             * @return the components
207             */
208            @SuppressWarnings("unchecked")
209            public <T extends ICalComponent> List<T> getComponents(Class<T> clazz) {
210                    List<ICalComponent> comp = components.get(clazz);
211    
212                    //cast to the requested class
213                    List<T> ret = new ArrayList<T>(comp.size());
214                    for (ICalComponent property : comp) {
215                            ret.add((T) property);
216                    }
217                    return ret;
218            }
219    
220            /**
221             * Gets all the sub-components associated with this component.
222             * @return the sub-components
223             */
224            public ListMultimap<Class<? extends ICalComponent>, ICalComponent> getComponents() {
225                    return components;
226            }
227    
228            /**
229             * Adds a sub-component to this component.
230             * @param component the component to add
231             */
232            public void addComponent(ICalComponent component) {
233                    components.put(component.getClass(), component);
234            }
235    
236            /**
237             * Replaces all components of a given class with the given component.
238             * @param component the component
239             */
240            public void setComponent(ICalComponent component) {
241                    components.replace(component.getClass(), component);
242            }
243    
244            /**
245             * Replaces all components of a given class with the given component. If the
246             * component instance is null, then all instances of that component will be
247             * removed.
248             * @param component the component or null to remove
249             */
250            public <T extends ICalComponent> void setComponent(Class<T> clazz, T component) {
251                    components.replace(clazz, component);
252            }
253    
254            /**
255             * Gets the first experimental sub-component with a given name.
256             * @param name the component name (e.g. "X-PARTY")
257             * @return the component or null if none were found
258             */
259            public RawComponent getExperimentalComponent(String name) {
260                    for (RawComponent raw : getComponents(RawComponent.class)) {
261                            if (raw.getName().equalsIgnoreCase(name)) {
262                                    return raw;
263                            }
264                    }
265                    return null;
266            }
267    
268            /**
269             * Gets all experimental sub-component with a given name.
270             * @param name the component name (e.g. "X-PARTY")
271             * @return the components
272             */
273            public List<RawComponent> getExperimentalComponents(String name) {
274                    List<RawComponent> props = new ArrayList<RawComponent>();
275    
276                    for (RawComponent raw : getComponents(RawComponent.class)) {
277                            if (raw.getName().equalsIgnoreCase(name)) {
278                                    props.add(raw);
279                            }
280                    }
281    
282                    return props;
283            }
284    
285            /**
286             * Gets all experimental sub-components associated with this component.
287             * @return the sub-components
288             */
289            public List<RawComponent> getExperimentalComponents() {
290                    return getComponents(RawComponent.class);
291            }
292    
293            /**
294             * Adds an experimental sub-component to this component.
295             * @param name the component name (e.g. "X-PARTY")
296             * @return the component object that was created
297             */
298            public RawComponent addExperimentalComponent(String name) {
299                    RawComponent raw = new RawComponent(name); //TODO rename to XComponent
300                    addComponent(raw);
301                    return raw;
302            }
303    
304            /**
305             * Adds an experimental sub-component to this component, removing all
306             * existing components that have the same name.
307             * @param name the component name (e.g. "X-PARTY")
308             * @return the component object that was created
309             */
310            public RawComponent setExperimentalComponents(String name) {
311                    removeExperimentalComponents(name);
312                    RawComponent raw = new RawComponent(name);
313                    addComponent(raw);
314                    return raw;
315            }
316    
317            /**
318             * Removes all experimental sub-components that have the given name.
319             * @param name the component name (e.g. "X-PARTY")
320             */
321            public void removeExperimentalComponents(String name) {
322                    List<RawComponent> xcomponents = getExperimentalComponents(name);
323                    for (RawComponent xcomponent : xcomponents) {
324                            components.remove(xcomponent.getClass(), xcomponent);
325                    }
326            }
327    
328            /**
329             * Checks the component for data consistency problems or deviations from the
330             * spec. These problems will not prevent the component from being written to
331             * a data stream, but may prevent it from being parsed correctly by the
332             * consuming application. These problems can largely be avoided by reading
333             * the Javadocs of the component class, or by being familiar with the
334             * iCalendar standard.
335             * @param hierarchy the hierarchy of components that the component belongs
336             * to
337             * @see ICalendar#validate
338             * @return a list of warnings or an empty list if no problems were found
339             */
340            public final List<String> validate(List<ICalComponent> hierarchy) {
341                    List<String> warnings = new ArrayList<String>();
342    
343                    //build the component path (e.g. "ICalendar > VEvent > VTimezone")
344                    String path;
345                    if (hierarchy.isEmpty()) {
346                            path = getClass().getSimpleName();
347                    } else {
348                            path = StringUtils.join(hierarchy, " > ", new JoinCallback<ICalComponent>() {
349                                    public void handle(StringBuilder sb, ICalComponent value) {
350                                            sb.append(value.getClass().getSimpleName());
351                                    }
352                            }) + " > " + getClass().getSimpleName(); //can't add "this" to "hierarchy" yet
353                    }
354    
355                    //validate this component
356                    List<String> thisWarnings = new ArrayList<String>();
357                    validate(hierarchy, thisWarnings);
358                    for (String warning : thisWarnings) {
359                            warnings.add("[" + path + "]: " + warning);
360                    }
361    
362                    //add this component to the stack
363                    //copy list so other validate() calls aren't effected
364                    hierarchy = new ArrayList<ICalComponent>(hierarchy);
365                    hierarchy.add(this);
366    
367                    //validate properties
368                    for (ICalProperty property : properties.values()) {
369                            List<String> propWarnings = property.validate(hierarchy);
370                            for (String warning : propWarnings) {
371                                    warnings.add("[" + path + " > " + property.getClass().getSimpleName() + "]: " + warning);
372                            }
373                    }
374    
375                    //validate sub-components
376                    for (ICalComponent component : components.values()) {
377                            warnings.addAll(component.validate(hierarchy));
378                    }
379    
380                    return warnings;
381            }
382    
383            /**
384             * Checks the component for data consistency problems or deviations from the
385             * spec. Meant to be overridden by child classes.
386             * @param components the hierarchy of components that the component belongs
387             * to
388             * @param warnings the list to add the warnings to
389             */
390            protected void validate(List<ICalComponent> components, List<String> warnings) {
391                    //do nothing
392            }
393    
394            /**
395             * Utility method for validating that there is exactly one instance of each
396             * of the given properties.
397             * @param warnings the list to add the warnings to
398             * @param classes the properties to check
399             */
400            protected void checkRequiredCardinality(List<String> warnings, Class<? extends ICalProperty>... classes) {
401                    for (Class<? extends ICalProperty> clazz : classes) {
402                            List<? extends ICalProperty> props = getProperties(clazz);
403    
404                            if (props.isEmpty()) {
405                                    warnings.add(clazz.getSimpleName() + " is not set (it is a required property).");
406                            } else if (props.size() > 1) {
407                                    warnings.add("There cannot be more than one instance of " + clazz.getSimpleName() + ".");
408                            }
409                    }
410            }
411    
412            /**
413             * Utility method for validating that there is no more than one instance of
414             * each of the given properties.
415             * @param warnings the list to add the warnings to
416             * @param classes the properties to check
417             */
418            protected void checkOptionalCardinality(List<String> warnings, Class<? extends ICalProperty>... classes) {
419                    for (Class<? extends ICalProperty> clazz : classes) {
420                            List<? extends ICalProperty> props = getProperties(clazz);
421    
422                            if (props.size() > 1) {
423                                    warnings.add("There cannot be more than one instance of " + clazz.getSimpleName() + ".");
424                            }
425                    }
426            }
427    }