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 }