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