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