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 }