001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.tools.tasks;
028
029import static org.opends.messages.AdminToolMessages.*;
030import static org.opends.messages.ToolMessages.*;
031
032import java.text.ParseException;
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.Date;
036import java.util.HashSet;
037import java.util.List;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.opends.quicksetup.util.PlainTextProgressMessageFormatter;
041import org.opends.quicksetup.util.ProgressMessageFormatter;
042import org.opends.server.admin.client.cli.TaskScheduleArgs;
043import org.opends.server.backends.task.FailedDependencyAction;
044import org.opends.server.backends.task.RecurringTask;
045import org.opends.server.types.DirectoryException;
046import org.opends.server.util.StaticUtils;
047
048import com.forgerock.opendj.cli.ClientException;
049import com.forgerock.opendj.cli.ReturnCode;
050
051import com.forgerock.opendj.cli.ConsoleApplication;
052import com.forgerock.opendj.cli.MenuBuilder;
053import com.forgerock.opendj.cli.MenuResult;
054
055/**
056 * A class that is in charge of interacting with the user to ask about
057 * scheduling options for a task.
058 * <br>
059 * It takes as argument an {@link TaskScheduleArgs} object with the arguments
060 * provided by the user and updates the provided {@link TaskScheduleUserData}
061 * with the information provided by the user.
062 *
063 */
064public class TaskScheduleInteraction
065{
066  private boolean headerDisplayed;
067  private final TaskScheduleUserData uData;
068  private final TaskScheduleArgs args;
069  private final ConsoleApplication app;
070  private final LocalizableMessage taskName;
071  private List<? extends TaskEntry> taskEntries =
072    Collections.emptyList();
073  private ProgressMessageFormatter formatter =
074    new PlainTextProgressMessageFormatter();
075
076  /**
077   * The enumeration used by the menu displayed to ask the user about the
078   * type of scheduling (if any) to be done.
079   *
080   */
081  private enum ScheduleOption {
082    RUN_NOW(INFO_RUN_TASK_NOW.get()),
083    RUN_LATER(INFO_RUN_TASK_LATER.get()),
084    SCHEDULE_TASK(INFO_SCHEDULE_TASK.get());
085
086    private LocalizableMessage prompt;
087    private ScheduleOption(LocalizableMessage prompt)
088    {
089      this.prompt = prompt;
090    }
091    LocalizableMessage getPrompt()
092    {
093      return prompt;
094    }
095
096    /**
097     * The default option to be proposed to the user.
098     * @return the default option to be proposed to the user.
099     */
100    public static ScheduleOption defaultValue()
101    {
102      return RUN_NOW;
103    }
104  }
105
106  /**
107   * Default constructor.
108   * @param uData the task schedule user data.
109   * @param args the object with the arguments provided by the user.  The code
110   * assumes that the arguments have already been parsed.
111   * @param app the console application object used to prompt for data.
112   * @param taskName the name of the task to be used in the prompt messages.
113   */
114  public TaskScheduleInteraction(TaskScheduleUserData uData,
115      TaskScheduleArgs args, ConsoleApplication app,
116      LocalizableMessage taskName)
117  {
118    this.uData = uData;
119    this.args = args;
120    this.app = app;
121    this.taskName = taskName;
122  }
123
124  /**
125   * Executes the interaction with the user.
126   * @throws ClientException if there is an error prompting the user.
127   */
128  public void run() throws ClientException
129  {
130    headerDisplayed = false;
131
132    runStartNowOrSchedule();
133
134    runCompletionNotification();
135
136    runErrorNotification();
137
138    runDependency();
139
140    if (!uData.getDependencyIds().isEmpty())
141    {
142      runFailedDependencyAction();
143    }
144  }
145
146  /**
147   * Returns the task entries that are defined in the server.  These are
148   * used to prompt the user about the task dependencies.
149   * @return the task entries that are defined in the server.
150   */
151  public List<? extends TaskEntry> getTaskEntries()
152  {
153    return taskEntries;
154  }
155
156  /**
157   * Sets the task entries that are defined in the server.  These are
158   * used to prompt the user about the task dependencies.  If no task entries
159   * are provided, the user will not be prompted for task dependencies.
160   * @param taskEntries the task entries that are defined in the server.
161   */
162  public void setTaskEntries(List<? extends TaskEntry> taskEntries)
163  {
164    this.taskEntries = taskEntries;
165  }
166
167  /**
168   * Returns the formatter that is used to generate messages.
169   * @return the formatter that is used to generate messages.
170   */
171  public ProgressMessageFormatter getFormatter()
172  {
173    return formatter;
174  }
175
176  /**
177   * Sets the formatter that is used to generate messages.
178   * @param formatter the formatter that is used to generate messages.
179   */
180  public void setFormatter(ProgressMessageFormatter formatter)
181  {
182    this.formatter = formatter;
183  }
184
185  private void runFailedDependencyAction() throws ClientException
186  {
187    if (args.dependencyArg.isPresent())
188    {
189      uData.setFailedDependencyAction(args.getFailedDependencyAction());
190    }
191    else
192    {
193      askForFailedDependencyAction();
194    }
195  }
196
197  private void askForFailedDependencyAction() throws ClientException
198  {
199    checkHeaderDisplay();
200
201    MenuBuilder<FailedDependencyAction> builder = new MenuBuilder<>(app);
202    builder.setPrompt(INFO_TASK_FAILED_DEPENDENCY_ACTION_PROMPT.get());
203    builder.addCancelOption(false);
204    for (FailedDependencyAction choice : FailedDependencyAction.values())
205    {
206      MenuResult<FailedDependencyAction> result = MenuResult.success(choice);
207
208      builder.addNumberedOption(choice.getDisplayName(), result);
209
210      if (choice.equals(FailedDependencyAction.defaultValue()))
211      {
212        builder.setDefault(choice.getDisplayName(), result);
213      }
214    }
215    MenuResult<FailedDependencyAction> m = builder.toMenu().run();
216    if (m.isSuccess())
217    {
218      uData.setFailedDependencyAction(m.getValue());
219    }
220    else
221    {
222      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, LocalizableMessage.EMPTY);
223    }
224  }
225
226  private void runDependency() throws ClientException
227  {
228    if (args.dependencyArg.isPresent())
229    {
230      uData.setDependencyIds(args.getDependencyIds());
231    }
232    else if (!taskEntries.isEmpty())
233    {
234      askForDependency();
235    }
236  }
237
238  private void askForDependency() throws ClientException
239  {
240    checkHeaderDisplay();
241
242    boolean hasDependencies =
243      app.confirmAction(INFO_TASK_HAS_DEPENDENCIES_PROMPT.get(), false);
244    if (hasDependencies)
245    {
246      printAvailableDependencyTaskMessage();
247      HashSet<String> dependencies = new HashSet<>();
248      while (true)
249      {
250        String dependencyID =
251          app.readLineOfInput(INFO_TASK_DEPENDENCIES_PROMPT.get());
252        if (dependencyID != null && !dependencyID.isEmpty())
253        {
254          if (isTaskIDDefined(dependencyID))
255          {
256            dependencies.add(dependencyID);
257          }
258          else
259          {
260            printTaskIDNotDefinedMessage(dependencyID);
261          }
262        }
263        else
264        {
265          break;
266        }
267      }
268      uData.setDependencyIds(new ArrayList<String>(dependencies));
269    }
270    else
271    {
272      List<String> empty = Collections.emptyList();
273      uData.setDependencyIds(empty);
274    }
275  }
276
277  private void printAvailableDependencyTaskMessage()
278  {
279    StringBuilder sb = new StringBuilder();
280    String separator = formatter.getLineBreak().toString() + formatter.getTab();
281    for (TaskEntry entry : taskEntries)
282    {
283      sb.append(separator);
284      sb.append(entry.getId());
285    }
286    app.println();
287    app.print(INFO_AVAILABLE_DEFINED_TASKS.get(sb));
288    app.println();
289    app.println();
290
291  }
292
293  private void printTaskIDNotDefinedMessage(String dependencyID)
294  {
295    app.println();
296    app.println(ERR_DEPENDENCY_TASK_NOT_DEFINED.get(dependencyID));
297  }
298
299  private boolean isTaskIDDefined(String dependencyID)
300  {
301    boolean taskIDDefined = false;
302    for (TaskEntry entry : taskEntries)
303    {
304      if (dependencyID.equalsIgnoreCase(entry.getId()))
305      {
306        taskIDDefined = true;
307        break;
308      }
309    }
310    return taskIDDefined;
311  }
312
313  private void runErrorNotification() throws ClientException
314  {
315    if (args.errorNotificationArg.isPresent())
316    {
317      uData.setNotifyUponErrorEmailAddresses(
318          args.getNotifyUponErrorEmailAddresses());
319    }
320    else
321    {
322      askForErrorNotification();
323    }
324  }
325
326  private void askForErrorNotification() throws ClientException
327  {
328    List<String> addresses =
329      askForEmailNotification(INFO_HAS_ERROR_NOTIFICATION_PROMPT.get(),
330          INFO_ERROR_NOTIFICATION_PROMPT.get());
331    uData.setNotifyUponErrorEmailAddresses(addresses);
332  }
333
334  private List<String> askForEmailNotification(LocalizableMessage hasNotificationPrompt,
335      LocalizableMessage emailAddressPrompt) throws ClientException
336  {
337    checkHeaderDisplay();
338
339    List<String> addresses = new ArrayList<>();
340    boolean hasNotification =
341      app.confirmAction(hasNotificationPrompt, false);
342    if (hasNotification)
343    {
344      HashSet<String> set = new HashSet<>();
345      while (true)
346      {
347        String address = app.readLineOfInput(emailAddressPrompt);
348        if (address == null || address.isEmpty())
349        {
350          break;
351        }
352        if (!StaticUtils.isEmailAddress(address)) {
353          app.println(ERR_INVALID_EMAIL_ADDRESS.get(address));
354        }
355        else
356        {
357          set.add(address);
358        }
359      }
360      addresses.addAll(set);
361    }
362    return addresses;
363  }
364
365  private void runCompletionNotification() throws ClientException
366  {
367    if (args.completionNotificationArg.isPresent())
368    {
369      uData.setNotifyUponCompletionEmailAddresses(
370          args.getNotifyUponCompletionEmailAddresses());
371    }
372    else
373    {
374      askForCompletionNotification();
375    }
376  }
377
378  private void askForCompletionNotification() throws ClientException
379  {
380    List<String> addresses =
381      askForEmailNotification(INFO_HAS_COMPLETION_NOTIFICATION_PROMPT.get(),
382          INFO_COMPLETION_NOTIFICATION_PROMPT.get());
383    uData.setNotifyUponCompletionEmailAddresses(addresses);
384  }
385
386  private void runStartNowOrSchedule() throws ClientException
387  {
388    if (args.startArg.isPresent())
389    {
390      uData.setStartDate(args.getStartDateTime());
391      uData.setStartNow(args.isStartNow());
392    }
393    if (args.recurringArg.isPresent())
394    {
395      uData.setRecurringDateTime(args.getRecurringDateTime());
396      uData.setStartNow(false);
397    }
398    if (!args.startArg.isPresent() &&
399        !args.recurringArg.isPresent())
400    {
401      askToStartNowOrSchedule();
402    }
403  }
404
405  private void askToStartNowOrSchedule() throws ClientException
406  {
407    checkHeaderDisplay();
408
409    MenuBuilder<ScheduleOption> builder = new MenuBuilder<>(app);
410    builder.setPrompt(INFO_TASK_SCHEDULE_PROMPT.get(taskName));
411    builder.addCancelOption(false);
412    for (ScheduleOption choice : ScheduleOption.values())
413    {
414      MenuResult<ScheduleOption> result = MenuResult.success(choice);
415      if (choice == ScheduleOption.defaultValue())
416      {
417        builder.setDefault(choice.getPrompt(), result);
418      }
419      builder.addNumberedOption(choice.getPrompt(), result);
420    }
421    MenuResult<ScheduleOption> m = builder.toMenu().run();
422    if (m.isSuccess())
423    {
424      switch (m.getValue())
425      {
426        case RUN_NOW:
427          uData.setStartNow(true);
428          break;
429        case RUN_LATER:
430          uData.setStartNow(false);
431          askForStartDate();
432          break;
433        case SCHEDULE_TASK:
434          uData.setStartNow(false);
435          askForTaskSchedule();
436          break;
437      }
438    }
439    else
440    {
441      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, LocalizableMessage.EMPTY);
442    }
443  }
444
445  private void askForStartDate() throws ClientException
446  {
447    checkHeaderDisplay();
448
449    Date startDate = null;
450    while (startDate == null)
451    {
452      String sDate = app.readInput(INFO_TASK_START_DATE_PROMPT.get(), null);
453      try {
454        startDate = StaticUtils.parseDateTimeString(sDate);
455        // Check that the provided date is not previous to the current date.
456        Date currentDate = new Date(System.currentTimeMillis());
457        if (currentDate.after(startDate))
458        {
459          app.print(ERR_START_DATETIME_ALREADY_PASSED.get(sDate));
460          app.println();
461          app.println();
462          startDate = null;
463        }
464      } catch (ParseException pe) {
465        app.println(ERR_START_DATETIME_FORMAT.get());
466        app.println();
467      }
468    }
469    uData.setStartDate(startDate);
470  }
471
472  private void askForTaskSchedule() throws ClientException
473  {
474    checkHeaderDisplay();
475
476    String schedule = null;
477
478    while (schedule == null)
479    {
480      schedule = app.readInput(INFO_TASK_RECURRING_SCHEDULE_PROMPT.get(),
481          null);
482      try
483      {
484        RecurringTask.parseTaskTab(schedule);
485        app.println();
486      }
487      catch (DirectoryException de)
488      {
489        schedule = null;
490        app.println(ERR_RECURRING_SCHEDULE_FORMAT_ERROR.get(
491            de.getMessageObject()));
492        app.println();
493      }
494    }
495    uData.setRecurringDateTime(schedule);
496  }
497
498  private void checkHeaderDisplay()
499  {
500    if (!headerDisplayed)
501    {
502      app.println();
503      app.print(INFO_TASK_SCHEDULE_PROMPT_HEADER.get());
504      app.println();
505      headerDisplayed = true;
506    }
507    app.println();
508
509  }
510}