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}