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 }