|
1 /** |
|
2 * Copyright (c) 2012, Ben Fortuna |
|
3 * All rights reserved. |
|
4 * |
|
5 * Redistribution and use in source and binary forms, with or without |
|
6 * modification, are permitted provided that the following conditions |
|
7 * are met: |
|
8 * |
|
9 * o Redistributions of source code must retain the above copyright |
|
10 * notice, this list of conditions and the following disclaimer. |
|
11 * |
|
12 * o Redistributions in binary form must reproduce the above copyright |
|
13 * notice, this list of conditions and the following disclaimer in the |
|
14 * documentation and/or other materials provided with the distribution. |
|
15 * |
|
16 * o Neither the name of Ben Fortuna nor the names of any other contributors |
|
17 * may be used to endorse or promote products derived from this software |
|
18 * without specific prior written permission. |
|
19 * |
|
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
31 */ |
|
32 package net.fortuna.ical4j.model; |
|
33 |
|
34 import java.io.IOException; |
|
35 import java.io.Serializable; |
|
36 import java.net.URISyntaxException; |
|
37 import java.text.ParseException; |
|
38 import java.util.Iterator; |
|
39 |
|
40 import net.fortuna.ical4j.model.parameter.Value; |
|
41 import net.fortuna.ical4j.model.property.DateProperty; |
|
42 import net.fortuna.ical4j.model.property.DtStart; |
|
43 import net.fortuna.ical4j.model.property.Duration; |
|
44 import net.fortuna.ical4j.model.property.ExDate; |
|
45 import net.fortuna.ical4j.model.property.ExRule; |
|
46 import net.fortuna.ical4j.model.property.RDate; |
|
47 import net.fortuna.ical4j.model.property.RRule; |
|
48 import net.fortuna.ical4j.util.Strings; |
|
49 |
|
50 import org.apache.commons.lang.builder.EqualsBuilder; |
|
51 import org.apache.commons.lang.builder.HashCodeBuilder; |
|
52 |
|
53 /** |
|
54 * $Id$ [Apr 5, 2004] |
|
55 * |
|
56 * Defines an iCalendar component. Subclasses of this class provide additional validation and typed values for specific |
|
57 * iCalendar components. |
|
58 * @author Ben Fortuna |
|
59 */ |
|
60 public abstract class Component implements Serializable { |
|
61 |
|
62 private static final long serialVersionUID = 4943193483665822201L; |
|
63 |
|
64 /** |
|
65 * Component start token. |
|
66 */ |
|
67 public static final String BEGIN = "BEGIN"; |
|
68 |
|
69 /** |
|
70 * Component end token. |
|
71 */ |
|
72 public static final String END = "END"; |
|
73 |
|
74 /** |
|
75 * Component token. |
|
76 */ |
|
77 public static final String VEVENT = "VEVENT"; |
|
78 |
|
79 /** |
|
80 * Component token. |
|
81 */ |
|
82 public static final String VTODO = "VTODO"; |
|
83 |
|
84 /** |
|
85 * Component token. |
|
86 */ |
|
87 public static final String VJOURNAL = "VJOURNAL"; |
|
88 |
|
89 /** |
|
90 * Component token. |
|
91 */ |
|
92 public static final String VFREEBUSY = "VFREEBUSY"; |
|
93 |
|
94 /** |
|
95 * Component token. |
|
96 */ |
|
97 public static final String VTIMEZONE = "VTIMEZONE"; |
|
98 |
|
99 /** |
|
100 * Component token. |
|
101 */ |
|
102 public static final String VALARM = "VALARM"; |
|
103 |
|
104 /** |
|
105 * Component token. |
|
106 */ |
|
107 public static final String VAVAILABILITY = "VAVAILABILITY"; |
|
108 |
|
109 /** |
|
110 * Component token. |
|
111 */ |
|
112 public static final String VVENUE = "VVENUE"; |
|
113 |
|
114 /** |
|
115 * Component token. |
|
116 */ |
|
117 public static final String AVAILABLE = "AVAILABLE"; |
|
118 |
|
119 /** |
|
120 * Prefix for non-standard components. |
|
121 */ |
|
122 public static final String EXPERIMENTAL_PREFIX = "X-"; |
|
123 |
|
124 private String name; |
|
125 |
|
126 private PropertyList properties; |
|
127 |
|
128 /** |
|
129 * Constructs a new component containing no properties. |
|
130 * @param s a component name |
|
131 */ |
|
132 protected Component(final String s) { |
|
133 this(s, new PropertyList()); |
|
134 } |
|
135 |
|
136 /** |
|
137 * Constructor made protected to enforce the use of <code>ComponentFactory</code> for component instantiation. |
|
138 * @param s component name |
|
139 * @param p a list of properties |
|
140 */ |
|
141 protected Component(final String s, final PropertyList p) { |
|
142 this.name = s; |
|
143 this.properties = p; |
|
144 } |
|
145 |
|
146 /** |
|
147 * {@inheritDoc} |
|
148 */ |
|
149 public String toString() { |
|
150 final StringBuffer buffer = new StringBuffer(); |
|
151 buffer.append(BEGIN); |
|
152 buffer.append(':'); |
|
153 buffer.append(getName()); |
|
154 buffer.append(Strings.LINE_SEPARATOR); |
|
155 buffer.append(getProperties()); |
|
156 buffer.append(END); |
|
157 buffer.append(':'); |
|
158 buffer.append(getName()); |
|
159 buffer.append(Strings.LINE_SEPARATOR); |
|
160 |
|
161 return buffer.toString(); |
|
162 } |
|
163 |
|
164 /** |
|
165 * @return Returns the name. |
|
166 */ |
|
167 public final String getName() { |
|
168 return name; |
|
169 } |
|
170 |
|
171 /** |
|
172 * @return Returns the properties. |
|
173 */ |
|
174 public final PropertyList getProperties() { |
|
175 return properties; |
|
176 } |
|
177 |
|
178 /** |
|
179 * Convenience method for retrieving a list of named properties. |
|
180 * @param name name of properties to retrieve |
|
181 * @return a property list containing only properties with the specified name |
|
182 */ |
|
183 public final PropertyList getProperties(final String name) { |
|
184 return getProperties().getProperties(name); |
|
185 } |
|
186 |
|
187 /** |
|
188 * Convenience method for retrieving a named property. |
|
189 * @param name name of the property to retrieve |
|
190 * @return the first matching property in the property list with the specified name |
|
191 */ |
|
192 public final Property getProperty(final String name) { |
|
193 return getProperties().getProperty(name); |
|
194 } |
|
195 |
|
196 /** |
|
197 * Perform validation on a component and its properties. |
|
198 * @throws ValidationException where the component is not in a valid state |
|
199 */ |
|
200 public final void validate() throws ValidationException { |
|
201 validate(true); |
|
202 } |
|
203 |
|
204 /** |
|
205 * Perform validation on a component. |
|
206 * @param recurse indicates whether to validate the component's properties |
|
207 * @throws ValidationException where the component is not in a valid state |
|
208 */ |
|
209 public abstract void validate(final boolean recurse) |
|
210 throws ValidationException; |
|
211 |
|
212 /** |
|
213 * Invoke validation on the component properties in its current state. |
|
214 * @throws ValidationException where any of the component properties is not in a valid state |
|
215 */ |
|
216 protected final void validateProperties() throws ValidationException { |
|
217 for (final Iterator i = getProperties().iterator(); i.hasNext();) { |
|
218 final Property property = (Property) i.next(); |
|
219 property.validate(); |
|
220 } |
|
221 } |
|
222 |
|
223 /** |
|
224 * {@inheritDoc} |
|
225 */ |
|
226 public boolean equals(final Object arg0) { |
|
227 if (arg0 instanceof Component) { |
|
228 final Component c = (Component) arg0; |
|
229 return new EqualsBuilder().append(getName(), c.getName()) |
|
230 .append(getProperties(), c.getProperties()).isEquals(); |
|
231 } |
|
232 return super.equals(arg0); |
|
233 } |
|
234 |
|
235 /** |
|
236 * {@inheritDoc} |
|
237 */ |
|
238 public int hashCode() { |
|
239 return new HashCodeBuilder().append(getName()).append(getProperties()) |
|
240 .toHashCode(); |
|
241 } |
|
242 |
|
243 /** |
|
244 * Create a (deep) copy of this component. |
|
245 * @return the component copy |
|
246 * @throws IOException where an error occurs reading the component data |
|
247 * @throws ParseException where parsing component data fails |
|
248 * @throws URISyntaxException where component data contains an invalid URI |
|
249 */ |
|
250 public Component copy() throws ParseException, IOException, |
|
251 URISyntaxException { |
|
252 |
|
253 // Deep copy properties.. |
|
254 final PropertyList newprops = new PropertyList(getProperties()); |
|
255 |
|
256 return ComponentFactory.getInstance().createComponent(getName(), |
|
257 newprops); |
|
258 } |
|
259 |
|
260 /** |
|
261 * Calculates the recurrence set for this component using the specified period. |
|
262 * The recurrence set is derived from a combination of the component start date, |
|
263 * recurrence rules and dates, and exception rules and dates. Note that component |
|
264 * transparency and anniversary-style dates do not affect the resulting |
|
265 * intersection. |
|
266 * <p>If an explicit DURATION is not specified, the effective duration of each |
|
267 * returned period is derived from the DTSTART and DTEND or DUE properties. |
|
268 * If the component has no DURATION, DTEND or DUE, the effective duration is set |
|
269 * to PT0S</p> |
|
270 * @param period a range to calculate recurrences for |
|
271 * @return a list of periods |
|
272 */ |
|
273 public final PeriodList calculateRecurrenceSet(final Period period) { |
|
274 |
|
275 // validate(); |
|
276 |
|
277 final PeriodList recurrenceSet = new PeriodList(); |
|
278 |
|
279 final DtStart start = (DtStart) getProperty(Property.DTSTART); |
|
280 DateProperty end = (DateProperty) getProperty(Property.DTEND); |
|
281 if (end == null) { |
|
282 end = (DateProperty) getProperty(Property.DUE); |
|
283 } |
|
284 Duration duration = (Duration) getProperty(Property.DURATION); |
|
285 |
|
286 // if no start date specified return empty list.. |
|
287 if (start == null) { |
|
288 return recurrenceSet; |
|
289 } |
|
290 |
|
291 final Value startValue = (Value) start.getParameter(Parameter.VALUE); |
|
292 |
|
293 // initialise timezone.. |
|
294 // if (startValue == null || Value.DATE_TIME.equals(startValue)) { |
|
295 if (start.isUtc()) { |
|
296 recurrenceSet.setUtc(true); |
|
297 } |
|
298 else if (start.getDate() instanceof DateTime) { |
|
299 recurrenceSet.setTimeZone(((DateTime) start.getDate()).getTimeZone()); |
|
300 } |
|
301 |
|
302 // if an explicit event duration is not specified, derive a value for recurring |
|
303 // periods from the end date.. |
|
304 Dur rDuration; |
|
305 // if no end or duration specified, end date equals start date.. |
|
306 if (end == null && duration == null) { |
|
307 rDuration = new Dur(start.getDate(), start.getDate()); |
|
308 } |
|
309 else if (duration == null) { |
|
310 rDuration = new Dur(start.getDate(), end.getDate()); |
|
311 } |
|
312 else { |
|
313 rDuration = duration.getDuration(); |
|
314 } |
|
315 |
|
316 // add recurrence dates.. |
|
317 for (final Iterator i = getProperties(Property.RDATE).iterator(); i.hasNext();) { |
|
318 final RDate rdate = (RDate) i.next(); |
|
319 final Value rdateValue = (Value) rdate.getParameter(Parameter.VALUE); |
|
320 if (Value.PERIOD.equals(rdateValue)) { |
|
321 for (final Iterator j = rdate.getPeriods().iterator(); j.hasNext();) { |
|
322 final Period rdatePeriod = (Period) j.next(); |
|
323 if (period.intersects(rdatePeriod)) { |
|
324 recurrenceSet.add(rdatePeriod); |
|
325 } |
|
326 } |
|
327 } |
|
328 else if (Value.DATE_TIME.equals(rdateValue)) { |
|
329 for (final Iterator j = rdate.getDates().iterator(); j.hasNext();) { |
|
330 final DateTime rdateTime = (DateTime) j.next(); |
|
331 if (period.includes(rdateTime)) { |
|
332 recurrenceSet.add(new Period(rdateTime, rDuration)); |
|
333 } |
|
334 } |
|
335 } |
|
336 else { |
|
337 for (final Iterator j = rdate.getDates().iterator(); j.hasNext();) { |
|
338 final Date rdateDate = (Date) j.next(); |
|
339 if (period.includes(rdateDate)) { |
|
340 recurrenceSet.add(new Period(new DateTime(rdateDate), rDuration)); |
|
341 } |
|
342 } |
|
343 } |
|
344 } |
|
345 |
|
346 // allow for recurrence rules that start prior to the specified period |
|
347 // but still intersect with it.. |
|
348 final DateTime startMinusDuration = new DateTime(period.getStart()); |
|
349 startMinusDuration.setTime(rDuration.negate().getTime( |
|
350 period.getStart()).getTime()); |
|
351 |
|
352 // add recurrence rules.. |
|
353 for (final Iterator i = getProperties(Property.RRULE).iterator(); i.hasNext();) { |
|
354 final RRule rrule = (RRule) i.next(); |
|
355 final DateList rruleDates = rrule.getRecur().getDates(start.getDate(), |
|
356 new Period(startMinusDuration, period.getEnd()), startValue); |
|
357 for (final Iterator j = rruleDates.iterator(); j.hasNext();) { |
|
358 final Date rruleDate = (Date) j.next(); |
|
359 recurrenceSet.add(new Period(new DateTime(rruleDate), rDuration)); |
|
360 } |
|
361 } |
|
362 |
|
363 // add initial instance if intersection with the specified period.. |
|
364 Period startPeriod = null; |
|
365 if (end != null) { |
|
366 startPeriod = new Period(new DateTime(start.getDate()), |
|
367 new DateTime(end.getDate())); |
|
368 } |
|
369 else { |
|
370 /* |
|
371 * PeS: Anniversary type has no DTEND nor DUR, define DUR |
|
372 * locally, otherwise we get NPE |
|
373 */ |
|
374 if (duration == null) { |
|
375 duration = new Duration(rDuration); |
|
376 } |
|
377 |
|
378 startPeriod = new Period(new DateTime(start.getDate()), |
|
379 duration.getDuration()); |
|
380 } |
|
381 if (period.intersects(startPeriod)) { |
|
382 recurrenceSet.add(startPeriod); |
|
383 } |
|
384 |
|
385 // subtract exception dates.. |
|
386 for (final Iterator i = getProperties(Property.EXDATE).iterator(); i.hasNext();) { |
|
387 final ExDate exdate = (ExDate) i.next(); |
|
388 for (final Iterator j = recurrenceSet.iterator(); j.hasNext();) { |
|
389 final Period recurrence = (Period) j.next(); |
|
390 // for DATE-TIME instances check for DATE-based exclusions also.. |
|
391 if (exdate.getDates().contains(recurrence.getStart()) |
|
392 || exdate.getDates().contains(new Date(recurrence.getStart()))) { |
|
393 j.remove(); |
|
394 } |
|
395 } |
|
396 } |
|
397 |
|
398 // subtract exception rules.. |
|
399 for (final Iterator i = getProperties(Property.EXRULE).iterator(); i.hasNext();) { |
|
400 final ExRule exrule = (ExRule) i.next(); |
|
401 final DateList exruleDates = exrule.getRecur().getDates(start.getDate(), |
|
402 period, startValue); |
|
403 for (final Iterator j = recurrenceSet.iterator(); j.hasNext();) { |
|
404 final Period recurrence = (Period) j.next(); |
|
405 // for DATE-TIME instances check for DATE-based exclusions also.. |
|
406 if (exruleDates.contains(recurrence.getStart()) |
|
407 || exruleDates.contains(new Date(recurrence.getStart()))) { |
|
408 j.remove(); |
|
409 } |
|
410 } |
|
411 } |
|
412 |
|
413 return recurrenceSet; |
|
414 } |
|
415 } |