001    package biweekly.component;
002    
003    import java.util.Arrays;
004    import java.util.List;
005    
006    import biweekly.parameter.Related;
007    import biweekly.property.Action;
008    import biweekly.property.Attachment;
009    import biweekly.property.Attendee;
010    import biweekly.property.DateDue;
011    import biweekly.property.DateEnd;
012    import biweekly.property.DateStart;
013    import biweekly.property.Description;
014    import biweekly.property.DurationProperty;
015    import biweekly.property.Repeat;
016    import biweekly.property.Summary;
017    import biweekly.property.Trigger;
018    import biweekly.util.Duration;
019    
020    /*
021     Copyright (c) 2013, Michael Angstadt
022     All rights reserved.
023    
024     Redistribution and use in source and binary forms, with or without
025     modification, are permitted provided that the following conditions are met: 
026    
027     1. Redistributions of source code must retain the above copyright notice, this
028     list of conditions and the following disclaimer. 
029     2. Redistributions in binary form must reproduce the above copyright notice,
030     this list of conditions and the following disclaimer in the documentation
031     and/or other materials provided with the distribution. 
032    
033     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
034     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
035     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
037     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
039     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
040     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
041     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
042     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043     */
044    
045    /**
046     * <p>
047     * Defines a reminder for an event or to-do task. This class contains static
048     * factory methods to aid in the construction of valid alarms.
049     * </p>
050     * 
051     * <p>
052     * <b>Examples:</b>
053     * 
054     * <pre class="brush:java">
055     * //audio alarm
056     * Trigger trigger = ...
057     * Attachment sound = ...
058     * VAlarm audio = VAlarm.audio(trigger, sound);
059     * 
060     * //display alarm
061     * Trigger trigger = ...
062     * String message = &quot;Meeting at 1pm&quot;;
063     * VAlarm display = VAlarm.display(trigger, message);
064     * 
065     * //email alarm
066     * Trigger trigger = ...
067     * String subject = &quot;Reminder: Meeting at 1pm&quot;;
068     * String body = &quot;Team,\n\nThe team meeting scheduled for 1pm is about to start.  Snacks will be served!\n\nThanks,\nJohn&quot;;
069     * List&lt;String&gt; to = Arrays.asList(&quot;janedoe@example.com&quot;, &quot;bobsmith@example.com&quot;);
070     * VAlarm email = VAlarm.email(trigger, subject, body, to);
071     * </pre>
072     * 
073     * </p>
074     * @author Michael Angstadt
075     * @rfc 5545 p.71-6
076     */
077    public class VAlarm extends ICalComponent {
078            /**
079             * Creates a new alarm. Consider using one of the static factory methods
080             * instead.
081             * @param action the alarm action (e.g. "email")
082             * @param trigger the trigger
083             */
084            public VAlarm(Action action, Trigger trigger) {
085                    setAction(action);
086                    setTrigger(trigger);
087            }
088    
089            /**
090             * Creates an audio alarm.
091             * @param trigger the trigger
092             * @return the alarm
093             */
094            public static VAlarm audio(Trigger trigger) {
095                    return audio(trigger, null);
096            }
097    
098            /**
099             * Creates an audio alarm.
100             * @param trigger the trigger
101             * @param sound a sound to play when the alarm triggers
102             * @return the alarm
103             */
104            public static VAlarm audio(Trigger trigger, Attachment sound) {
105                    VAlarm alarm = new VAlarm(Action.audio(), trigger);
106                    if (sound != null) {
107                            alarm.addAttachment(sound);
108                    }
109                    return alarm;
110            }
111    
112            /**
113             * Creates a display alarm.
114             * @param trigger the trigger
115             * @param displayText the display text
116             * @return the alarm
117             */
118            public static VAlarm display(Trigger trigger, String displayText) {
119                    VAlarm alarm = new VAlarm(Action.display(), trigger);
120                    alarm.setDescription(displayText);
121                    return alarm;
122            }
123    
124            /**
125             * Creates an email alarm.
126             * @param trigger the trigger
127             * @param subject the email subject
128             * @param body the email body
129             * @param recipients the email address(es) to send the alert to
130             * @return the alarm
131             */
132            public static VAlarm email(Trigger trigger, String subject, String body, String... recipients) {
133                    return email(trigger, subject, body, Arrays.asList(recipients));
134            }
135    
136            /**
137             * Creates an email alarm.
138             * @param trigger the trigger
139             * @param subject the email subject
140             * @param body the email body
141             * @param recipients the email address(es) to send the alert to
142             * @return the alarm
143             */
144            public static VAlarm email(Trigger trigger, String subject, String body, List<String> recipients) {
145                    VAlarm alarm = new VAlarm(Action.email(), trigger);
146                    alarm.setSummary(subject);
147                    alarm.setDescription(body);
148                    for (String recipient : recipients) {
149                            alarm.addAttendee(Attendee.email(recipient));
150                    }
151                    return alarm;
152            }
153    
154            /**
155             * Gets any attachments that are associated with the alarm.
156             * @return the attachments
157             * @rfc 5545 p.80-1
158             */
159            public List<Attachment> getAttachments() {
160                    return getProperties(Attachment.class);
161            }
162    
163            /**
164             * Adds an attachment to the alarm. Note that AUDIO alarms should only have
165             * 1 attachment.
166             * @param attachment the attachment to add
167             * @rfc 5545 p.80-1
168             */
169            public void addAttachment(Attachment attachment) {
170                    addProperty(attachment);
171            }
172    
173            /**
174             * <p>
175             * Gets a detailed description of the alarm. The description should be more
176             * detailed than the one provided by the {@link Summary} property.
177             * </p>
178             * <p>
179             * This property has different meanings, depending on the alarm action:
180             * <ul>
181             * <li>DISPLAY - the display text</li>
182             * <li>EMAIL - the body of the email message</li>
183             * <li>all others - a general description of the alarm</li>
184             * </ul>
185             * </p>
186             * @return the description or null if not set
187             * @rfc 5545 p.84-5
188             */
189            public Description getDescription() {
190                    return getProperty(Description.class);
191            }
192    
193            /**
194             * <p>
195             * Sets a detailed description of the alarm. The description should be more
196             * detailed than the one provided by the {@link Summary} property.
197             * </p>
198             * <p>
199             * This property has different meanings, depending on the alarm action:
200             * <ul>
201             * <li>DISPLAY - the display text</li>
202             * <li>EMAIL - the body of the email message</li>
203             * <li>all others - a general description of the alarm</li>
204             * </ul>
205             * </p>
206             * @param description the description or null to remove
207             * @rfc 5545 p.84-5
208             */
209            public void setDescription(Description description) {
210                    setProperty(Description.class, description);
211            }
212    
213            /**
214             * <p>
215             * Sets a detailed description of the alarm. The description should be more
216             * detailed than the one provided by the {@link Summary} property.
217             * </p>
218             * <p>
219             * This property has different meanings, depending on the alarm action:
220             * <ul>
221             * <li>DISPLAY - the display text</li>
222             * <li>EMAIL - the body of the email message</li>
223             * <li>all others - a general description of the alarm</li>
224             * </ul>
225             * </p>
226             * @param description the description or null to remove
227             * @return the property that was created
228             * @rfc 5545 p.84-5
229             */
230            public Description setDescription(String description) {
231                    Description prop = (description == null) ? null : new Description(description);
232                    setDescription(prop);
233                    return prop;
234            }
235    
236            /**
237             * <p>
238             * Gets the summary of the alarm.
239             * </p>
240             * <p>
241             * This property has different meanings, depending on the alarm action:
242             * <ul>
243             * <li>EMAIL - the subject line of the email</li>
244             * <li>all others - a one-line summary of the alarm</li>
245             * </ul>
246             * </p>
247             * @return the summary or null if not set
248             * @rfc 5545 p.93-4
249             */
250            public Summary getSummary() {
251                    return getProperty(Summary.class);
252            }
253    
254            /**
255             * <p>
256             * Sets the summary of the alarm.
257             * </p>
258             * <p>
259             * This property has different meanings, depending on the alarm action:
260             * <ul>
261             * <li>EMAIL - the subject line of the email</li>
262             * <li>all others - a one-line summary of the alarm</li>
263             * </ul>
264             * </p>
265             * @param summary the summary or null to remove
266             * @rfc 5545 p.93-4
267             */
268            public void setSummary(Summary summary) {
269                    setProperty(Summary.class, summary);
270            }
271    
272            /**
273             * <p>
274             * Sets the summary of the alarm.
275             * </p>
276             * <p>
277             * This property has different meanings, depending on the alarm action:
278             * <ul>
279             * <li>EMAIL - the subject line of the email</li>
280             * <li>all others - a one-line summary of the alarm</li>
281             * </ul>
282             * </p>
283             * @param summary the summary or null to remove
284             * @return the property that was created
285             * @rfc 5545 p.93-4
286             */
287            public Summary setSummary(String summary) {
288                    Summary prop = (summary == null) ? null : new Summary(summary);
289                    setSummary(prop);
290                    return prop;
291            }
292    
293            /**
294             * Gets the people who will be emailed when the alarm fires (only applicable
295             * for EMAIL alarms).
296             * @return the email recipients
297             * @rfc 5545 p.107-9
298             */
299            public List<Attendee> getAttendees() {
300                    return getProperties(Attendee.class);
301            }
302    
303            /**
304             * Adds a person who will be emailed when the alarm fires (only applicable
305             * for EMAIL alarms).
306             * @param attendee the email recipient
307             * @rfc 5545 p.107-9
308             */
309            public void addAttendee(Attendee attendee) {
310                    addProperty(attendee);
311            }
312    
313            /**
314             * Gets the type of action to invoke when the alarm is triggered.
315             * @return the action or null if not set
316             * @rfc 5545 p.132-3
317             */
318            public Action getAction() {
319                    return getProperty(Action.class);
320            }
321    
322            /**
323             * Sets the type of action to invoke when the alarm is triggered.
324             * @param action the action or null to remove
325             * @rfc 5545 p.132-3
326             */
327            public void setAction(Action action) {
328                    setProperty(Action.class, action);
329            }
330    
331            /**
332             * Gets the length of the pause between alarm repetitions.
333             * @return the duration or null if not set
334             * @rfc 5545 p.99
335             */
336            public DurationProperty getDuration() {
337                    return getProperty(DurationProperty.class);
338            }
339    
340            /**
341             * Sets the length of the pause between alarm repetitions.
342             * @param duration the duration or null to remove
343             * @rfc 5545 p.99
344             */
345            public void setDuration(DurationProperty duration) {
346                    setProperty(DurationProperty.class, duration);
347            }
348    
349            /**
350             * Sets the length of the pause between alarm repetitions.
351             * @param duration the duration or null to remove
352             * @return the property that was created
353             * @rfc 5545 p.99
354             */
355            public DurationProperty setDuration(Duration duration) {
356                    DurationProperty prop = (duration == null) ? null : new DurationProperty(duration);
357                    setDuration(prop);
358                    return prop;
359            }
360    
361            /**
362             * Gets the number of times an alarm should be repeated after its initial
363             * trigger.
364             * @return the repeat count or null if not set
365             * @rfc 5545 p.133
366             */
367            public Repeat getRepeat() {
368                    return getProperty(Repeat.class);
369            }
370    
371            /**
372             * Sets the number of times an alarm should be repeated after its initial
373             * trigger.
374             * @param repeat the repeat count or null to remove
375             * @rfc 5545 p.133
376             */
377            public void setRepeat(Repeat repeat) {
378                    setProperty(Repeat.class, repeat);
379            }
380    
381            /**
382             * Sets the number of times an alarm should be repeated after its initial
383             * trigger.
384             * @param count the repeat count (e.g. "2" to repeat it two more times after
385             * it was initially triggered, for a total of three times) or null to remove
386             * @return the property that was created
387             * @rfc 5545 p.133
388             */
389            public Repeat setRepeat(Integer count) {
390                    Repeat prop = (count == null) ? null : new Repeat(count);
391                    setRepeat(prop);
392                    return prop;
393            }
394    
395            /**
396             * Sets the repetition information for the alarm.
397             * @param count the repeat count (e.g. "2" to repeat it two more times after
398             * it was initially triggered, for a total of three times)
399             * @param pauseDuration the length of the pause between repeats
400             * @rfc 5545 p.133
401             */
402            public void setRepeat(int count, Duration pauseDuration) {
403                    Repeat repeat = new Repeat(count);
404                    DurationProperty duration = new DurationProperty(pauseDuration);
405                    setRepeat(repeat);
406                    setDuration(duration);
407            }
408    
409            /**
410             * Gets when the alarm will be triggered.
411             * @return the trigger time or null if not set
412             * @rfc 5545 p.133-6
413             */
414            public Trigger getTrigger() {
415                    return getProperty(Trigger.class);
416            }
417    
418            /**
419             * Sets when the alarm will be triggered.
420             * @param trigger the trigger time or null to remove
421             * @rfc 5545 p.133-6
422             */
423            public void setTrigger(Trigger trigger) {
424                    setProperty(Trigger.class, trigger);
425            }
426    
427            @SuppressWarnings("unchecked")
428            @Override
429            protected void validate(List<ICalComponent> components, List<String> warnings) {
430                    //all alarm types require Action and Trigger
431                    checkRequiredCardinality(warnings, Action.class, Trigger.class);
432    
433                    Action action = getAction();
434                    if (action != null) {
435                            if (action.isAudio()) {
436                                    if (getAttachments().size() > 1) {
437                                            warnings.add("Audio alarms should have no more than 1 attachment.");
438                                    }
439                            }
440    
441                            if (action.isDisplay()) {
442                                    checkRequiredCardinality(warnings, Description.class);
443                            }
444    
445                            if (action.isEmail()) {
446                                    checkRequiredCardinality(warnings, Summary.class, Description.class);
447                                    if (getAttendees().isEmpty()) {
448                                            warnings.add("Email alarms must have at least one attendee.");
449                                    }
450                            } else {
451                                    if (!getAttendees().isEmpty()) {
452                                            warnings.add("Only email alarms can have attendees.");
453                                    }
454                            }
455                    }
456    
457                    Trigger trigger = getTrigger();
458                    if (trigger != null) {
459                            Related related = trigger.getRelated();
460    
461                            if (related == null && trigger.getDuration() != null) {
462                                    warnings.add("The trigger must specify which date field its duration is relative to.");
463                            }
464    
465                            if (related != null) {
466                                    ICalComponent parent = components.get(components.size() - 1);
467                                    if (related == Related.START && parent.getProperty(DateStart.class) == null) {
468                                            warnings.add("The trigger is settings its duration relative to the start date, but the parent component has no start date property.");
469                                    }
470                                    if (related == Related.END) {
471                                            boolean noEndDate = false;
472    
473                                            if (parent instanceof VEvent) {
474                                                    noEndDate = (parent.getProperty(DateEnd.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null));
475                                            } else if (parent instanceof VTodo) {
476                                                    noEndDate = (parent.getProperty(DateDue.class) == null && (parent.getProperty(DateStart.class) == null || parent.getProperty(DurationProperty.class) == null));
477                                            }
478    
479                                            if (noEndDate) {
480                                                    warnings.add("The trigger is settings its duration relative to the end date, but the parent component has no end date or equivalent set.");
481                                            }
482                                    }
483                            }
484                    }
485            }
486    }