001package biweekly.component; 002 003import java.util.ArrayList; 004import java.util.List; 005 006import biweekly.ICalDataType; 007import biweekly.ICalVersion; 008import biweekly.ICalendar; 009import biweekly.ValidationWarnings.WarningsGroup; 010import biweekly.Warning; 011import biweekly.property.ICalProperty; 012import biweekly.property.RawProperty; 013import biweekly.property.Status; 014import biweekly.util.ListMultimap; 015 016/* 017 Copyright (c) 2013-2015, Michael Angstadt 018 All rights reserved. 019 020 Redistribution and use in source and binary forms, with or without 021 modification, are permitted provided that the following conditions are met: 022 023 1. Redistributions of source code must retain the above copyright notice, this 024 list of conditions and the following disclaimer. 025 2. Redistributions in binary form must reproduce the above copyright notice, 026 this list of conditions and the following disclaimer in the documentation 027 and/or other materials provided with the distribution. 028 029 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 030 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 031 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 032 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 033 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 034 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 035 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 036 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 037 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 038 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 039 */ 040 041/** 042 * Base class for all iCalendar components. 043 * @author Michael Angstadt 044 */ 045public abstract class ICalComponent { 046 protected final ListMultimap<Class<? extends ICalComponent>, ICalComponent> components = new ListMultimap<Class<? extends ICalComponent>, ICalComponent>(); 047 protected final ListMultimap<Class<? extends ICalProperty>, ICalProperty> properties = new ListMultimap<Class<? extends ICalProperty>, ICalProperty>(); 048 049 /** 050 * Gets the first property of a given class. 051 * @param clazz the property class 052 * @return the property or null if not found 053 */ 054 public <T extends ICalProperty> T getProperty(Class<T> clazz) { 055 return clazz.cast(properties.first(clazz)); 056 } 057 058 /** 059 * Gets all properties of a given class. 060 * @param clazz the property class 061 * @return the properties 062 */ 063 public <T extends ICalProperty> List<T> getProperties(Class<T> clazz) { 064 List<ICalProperty> props = properties.get(clazz); 065 066 //cast to the requested class 067 List<T> ret = new ArrayList<T>(props.size()); 068 for (ICalProperty property : props) { 069 ret.add(clazz.cast(property)); 070 } 071 return ret; 072 } 073 074 /** 075 * Gets all the properties associated with this component. 076 * @return the properties 077 */ 078 public ListMultimap<Class<? extends ICalProperty>, ICalProperty> getProperties() { 079 return properties; 080 } 081 082 /** 083 * Adds a property to this component. 084 * @param property the property to add 085 */ 086 public void addProperty(ICalProperty property) { 087 properties.put(property.getClass(), property); 088 } 089 090 /** 091 * Replaces all existing properties of the given class with a single 092 * property instance. 093 * @param property the property (must not be null) 094 */ 095 public void setProperty(ICalProperty property) { 096 properties.replace(property.getClass(), property); 097 } 098 099 /** 100 * Replaces all existing properties of the given class with a single 101 * property instance. If the property instance is null, then all instances 102 * of that property will be removed. 103 * @param clazz the property class (e.g. "DateStart.class") 104 * @param property the property or null to remove 105 */ 106 public <T extends ICalProperty> void setProperty(Class<T> clazz, T property) { 107 properties.replace(clazz, property); 108 } 109 110 /** 111 * Removes properties from the iCalendar object. 112 * @param clazz the class of the properties to remove (e.g. 113 * "DateStart.class") 114 */ 115 public void removeProperties(Class<? extends ICalProperty> clazz) { 116 properties.removeAll(clazz); 117 } 118 119 /** 120 * Removes components from the iCalendar object. 121 * @param clazz the class of the components to remove (e.g. "VEvent.class") 122 */ 123 public void removeComponents(Class<? extends ICalComponent> clazz) { 124 components.removeAll(clazz); 125 } 126 127 /** 128 * Gets the first experimental property with a given name. 129 * @param name the property name (e.g. "X-ALT-DESC") 130 * @return the property or null if none were found 131 */ 132 public RawProperty getExperimentalProperty(String name) { 133 for (RawProperty raw : getProperties(RawProperty.class)) { 134 if (raw.getName().equalsIgnoreCase(name)) { 135 return raw; 136 } 137 } 138 return null; 139 } 140 141 /** 142 * Gets all experimental properties with a given name. 143 * @param name the property name (e.g. "X-ALT-DESC") 144 * @return the properties 145 */ 146 public List<RawProperty> getExperimentalProperties(String name) { 147 List<RawProperty> props = new ArrayList<RawProperty>(); 148 149 for (RawProperty raw : getProperties(RawProperty.class)) { 150 if (raw.getName().equalsIgnoreCase(name)) { 151 props.add(raw); 152 } 153 } 154 155 return props; 156 } 157 158 /** 159 * Gets all experimental properties associated with this component. 160 * @return the properties 161 */ 162 public List<RawProperty> getExperimentalProperties() { 163 return getProperties(RawProperty.class); 164 } 165 166 /** 167 * Adds an experimental property to this component. 168 * @param name the property name (e.g. "X-ALT-DESC") 169 * @param value the property value 170 * @return the property object that was created 171 */ 172 public RawProperty addExperimentalProperty(String name, String value) { 173 return addExperimentalProperty(name, null, value); 174 } 175 176 /** 177 * Adds an experimental property to this component. 178 * @param name the property name (e.g. "X-ALT-DESC") 179 * @param dataType the property's data type (e.g. "text") or null if unknown 180 * @param value the property value 181 * @return the property object that was created 182 */ 183 public RawProperty addExperimentalProperty(String name, ICalDataType dataType, String value) { 184 RawProperty raw = new RawProperty(name, dataType, value); 185 addProperty(raw); 186 return raw; 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 value the property value 194 * @return the property object that was created 195 */ 196 public RawProperty setExperimentalProperty(String name, String value) { 197 return setExperimentalProperty(name, null, value); 198 } 199 200 /** 201 * Adds an experimental property to this component, removing all existing 202 * properties that have the same name. 203 * @param name the property name (e.g. "X-ALT-DESC") 204 * @param dataType the property's data type (e.g. "text") or null if unknown 205 * @param value the property value 206 * @return the property object that was created 207 */ 208 public RawProperty setExperimentalProperty(String name, ICalDataType dataType, String value) { 209 removeExperimentalProperty(name); 210 return addExperimentalProperty(name, dataType, value); 211 } 212 213 /** 214 * Removes all experimental properties that have the given name. 215 * @param name the component name (e.g. "X-ALT-DESC") 216 */ 217 public void removeExperimentalProperty(String name) { 218 List<RawProperty> xproperties = getExperimentalProperties(name); 219 for (RawProperty xproperty : xproperties) { 220 properties.remove(xproperty.getClass(), xproperty); 221 } 222 } 223 224 /** 225 * Gets the first component of a given class. 226 * @param clazz the component class 227 * @return the component or null if not found 228 */ 229 public <T extends ICalComponent> T getComponent(Class<T> clazz) { 230 return clazz.cast(components.first(clazz)); 231 } 232 233 /** 234 * Gets all components of a given class. 235 * @param clazz the component class 236 * @return the components 237 */ 238 public <T extends ICalComponent> List<T> getComponents(Class<T> clazz) { 239 List<ICalComponent> comp = components.get(clazz); 240 241 //cast to the requested class 242 List<T> ret = new ArrayList<T>(comp.size()); 243 for (ICalComponent property : comp) { 244 ret.add(clazz.cast(property)); 245 } 246 return ret; 247 } 248 249 /** 250 * Gets all the sub-components associated with this component. 251 * @return the sub-components 252 */ 253 public ListMultimap<Class<? extends ICalComponent>, ICalComponent> getComponents() { 254 return components; 255 } 256 257 /** 258 * Adds a sub-component to this component. 259 * @param component the component to add 260 */ 261 public void addComponent(ICalComponent component) { 262 components.put(component.getClass(), component); 263 } 264 265 /** 266 * Replaces all components of a given class with the given component. 267 * @param component the component (must not be null) 268 */ 269 public void setComponent(ICalComponent component) { 270 components.replace(component.getClass(), component); 271 } 272 273 /** 274 * Replaces all components of a given class with the given component. If the 275 * component instance is null, then all instances of that component will be 276 * removed. 277 * @param clazz the component's class 278 * @param component the component or null to remove 279 */ 280 public <T extends ICalComponent> void setComponent(Class<T> clazz, T component) { 281 components.replace(clazz, component); 282 } 283 284 /** 285 * Gets the first experimental sub-component with a given name. 286 * @param name the component name (e.g. "X-PARTY") 287 * @return the component or null if none were found 288 */ 289 public RawComponent getExperimentalComponent(String name) { 290 for (RawComponent raw : getComponents(RawComponent.class)) { 291 if (raw.getName().equalsIgnoreCase(name)) { 292 return raw; 293 } 294 } 295 return null; 296 } 297 298 /** 299 * Gets all experimental sub-component with a given name. 300 * @param name the component name (e.g. "X-PARTY") 301 * @return the components 302 */ 303 public List<RawComponent> getExperimentalComponents(String name) { 304 List<RawComponent> props = new ArrayList<RawComponent>(); 305 306 for (RawComponent raw : getComponents(RawComponent.class)) { 307 if (raw.getName().equalsIgnoreCase(name)) { 308 props.add(raw); 309 } 310 } 311 312 return props; 313 } 314 315 /** 316 * Gets all experimental sub-components associated with this component. 317 * @return the sub-components 318 */ 319 public List<RawComponent> getExperimentalComponents() { 320 return getComponents(RawComponent.class); 321 } 322 323 /** 324 * Adds an experimental sub-component to this component. 325 * @param name the component name (e.g. "X-PARTY") 326 * @return the component object that was created 327 */ 328 public RawComponent addExperimentalComponent(String name) { 329 RawComponent raw = new RawComponent(name); 330 addComponent(raw); 331 return raw; 332 } 333 334 /** 335 * Adds an experimental sub-component to this component, removing all 336 * existing components that have the same name. 337 * @param name the component name (e.g. "X-PARTY") 338 * @return the component object that was created 339 */ 340 public RawComponent setExperimentalComponents(String name) { 341 removeExperimentalComponents(name); 342 return addExperimentalComponent(name); 343 } 344 345 /** 346 * Removes all experimental sub-components that have the given name. 347 * @param name the component name (e.g. "X-PARTY") 348 */ 349 public void removeExperimentalComponents(String name) { 350 List<RawComponent> xcomponents = getExperimentalComponents(name); 351 for (RawComponent xcomponent : xcomponents) { 352 components.remove(xcomponent.getClass(), xcomponent); 353 } 354 } 355 356 /** 357 * Checks the component for data consistency problems or deviations from the 358 * spec. These problems will not prevent the component from being written to 359 * a data stream, but may prevent it from being parsed correctly by the 360 * consuming application. These problems can largely be avoided by reading 361 * the Javadocs of the component class, or by being familiar with the 362 * iCalendar standard. 363 * @param hierarchy the hierarchy of components that the component belongs 364 * to 365 * @param version the version to validate against 366 * @see ICalendar#validate 367 * @return a list of warnings or an empty list if no problems were found 368 */ 369 public final List<WarningsGroup> validate(List<ICalComponent> hierarchy, ICalVersion version) { 370 List<WarningsGroup> warnings = new ArrayList<WarningsGroup>(); 371 372 //validate this component 373 List<Warning> warningsBuf = new ArrayList<Warning>(0); 374 validate(hierarchy, version, warningsBuf); 375 if (!warningsBuf.isEmpty()) { 376 warnings.add(new WarningsGroup(this, hierarchy, warningsBuf)); 377 } 378 379 //add this component to the hierarchy list 380 //copy the list so other validate() calls aren't effected 381 hierarchy = new ArrayList<ICalComponent>(hierarchy); 382 hierarchy.add(this); 383 384 //validate properties 385 for (ICalProperty property : properties.values()) { 386 List<Warning> propWarnings = property.validate(hierarchy, version); 387 if (!propWarnings.isEmpty()) { 388 warnings.add(new WarningsGroup(property, hierarchy, propWarnings)); 389 } 390 } 391 392 //validate sub-components 393 for (ICalComponent component : components.values()) { 394 warnings.addAll(component.validate(hierarchy, version)); 395 } 396 397 return warnings; 398 } 399 400 /** 401 * <p> 402 * Checks the component for data consistency problems or deviations from the 403 * spec. 404 * </p> 405 * <p> 406 * This method should be overridden by child classes that wish to provide 407 * validation logic. The default implementation of this method does nothing. 408 * </p> 409 * @param components the hierarchy of components that the component belongs 410 * to 411 * @param version the version to validate against 412 * @param warnings the list to add the warnings to 413 */ 414 protected void validate(List<ICalComponent> components, ICalVersion version, List<Warning> warnings) { 415 //do nothing 416 } 417 418 /** 419 * Utility method for validating that there is exactly one instance of each 420 * of the given properties. 421 * @param warnings the list to add the warnings to 422 * @param classes the properties to check 423 */ 424 protected void checkRequiredCardinality(List<Warning> warnings, Class<? extends ICalProperty>... classes) { 425 for (Class<? extends ICalProperty> clazz : classes) { 426 List<? extends ICalProperty> props = getProperties(clazz); 427 428 if (props.isEmpty()) { 429 warnings.add(Warning.validate(2, clazz.getSimpleName())); 430 continue; 431 } 432 433 if (props.size() > 1) { 434 warnings.add(Warning.validate(3, clazz.getSimpleName())); 435 continue; 436 } 437 } 438 } 439 440 /** 441 * Utility method for validating that there is no more than one instance of 442 * each of the given properties. 443 * @param warnings the list to add the warnings to 444 * @param classes the properties to check 445 */ 446 protected void checkOptionalCardinality(List<Warning> warnings, Class<? extends ICalProperty>... classes) { 447 for (Class<? extends ICalProperty> clazz : classes) { 448 List<? extends ICalProperty> props = getProperties(clazz); 449 450 if (props.size() > 1) { 451 warnings.add(Warning.validate(3, clazz.getSimpleName())); 452 continue; 453 } 454 } 455 } 456 457 /** 458 * Utility method for validating the {@link Status} property of a component. 459 * @param warnings the list to add the warnings to 460 * @param allowed the valid statuses 461 */ 462 protected void checkStatus(List<Warning> warnings, Status... allowed) { 463 Status actual = getProperty(Status.class); 464 if (actual == null) { 465 return; 466 } 467 468 List<String> allowedValues = new ArrayList<String>(allowed.length); 469 for (Status status : allowed) { 470 String value = status.getValue().toLowerCase(); 471 allowedValues.add(value); 472 } 473 474 String actualValue = actual.getValue().toLowerCase(); 475 if (!allowedValues.contains(actualValue)) { 476 warnings.add(Warning.validate(13, actual.getValue(), allowedValues)); 477 } 478 } 479}