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 }