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 }