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 2007-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS
026 */
027package org.opends.server.tools.tasks;
028
029import static org.opends.messages.TaskMessages.*;
030import static org.opends.messages.ToolMessages.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import static com.forgerock.opendj.cli.Utils.*;
034
035import java.io.IOException;
036import java.io.PrintStream;
037import java.util.Date;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Set;
041import java.util.logging.Level;
042
043import org.forgerock.i18n.LocalizableMessage;
044import org.forgerock.opendj.ldap.DecodeException;
045import org.opends.server.admin.client.cli.TaskScheduleArgs;
046import org.opends.server.backends.task.FailedDependencyAction;
047import org.opends.server.backends.task.TaskState;
048import org.opends.server.core.DirectoryServer;
049import org.opends.server.loggers.JDKLogging;
050import org.opends.server.tools.LDAPConnection;
051import org.opends.server.tools.LDAPConnectionException;
052import org.opends.server.types.InitializationException;
053import org.opends.server.types.LDAPException;
054import org.opends.server.types.OpenDsException;
055import org.opends.server.util.BuildVersion;
056import org.opends.server.util.args.LDAPConnectionArgumentParser;
057
058import com.forgerock.opendj.cli.Argument;
059import com.forgerock.opendj.cli.ArgumentException;
060import com.forgerock.opendj.cli.ArgumentGroup;
061import com.forgerock.opendj.cli.BooleanArgument;
062import com.forgerock.opendj.cli.ClientException;
063import com.forgerock.opendj.cli.CommonArguments;
064import com.forgerock.opendj.cli.StringArgument;
065
066/**
067 * Base class for tools that are capable of operating either by running
068 * local within this JVM or by scheduling a task to perform the same
069 * action running within the directory server through the tasks interface.
070 */
071public abstract class TaskTool implements TaskScheduleInformation {
072
073  /**
074   * Magic value used to indicate that the user would like to schedule
075   * this operation to run immediately as a task as opposed to running
076   * the operation in the local VM.
077   */
078  public static final String NOW = TaskScheduleArgs.NOW;
079
080  /**
081   * The error code used by the mixed-script to know if the java
082   * arguments for the off-line mode must be used.
083   */
084  private static final int RUN_OFFLINE = 51;
085  /**
086   * The error code used by the mixed-script to know if the java
087   * arguments for the on-line mode must be used.
088   */
089  private static final int RUN_ONLINE = 52;
090
091  /**
092   * Number of milliseconds this utility will wait before reloading
093   * this task's entry in the directory while it is polling for status.
094   */
095  private static final int SYNCHRONOUS_TASK_POLL_INTERVAL = 1000;
096
097  private LDAPConnectionArgumentParser argParser;
098
099  private TaskScheduleArgs taskScheduleArgs;
100
101  /**
102   * Argument used to know whether we must test if we must run in off-line mode.
103   */
104  private BooleanArgument testIfOfflineArg;
105
106  /** This CLI is always using the administration connector with SSL. */
107  private static final boolean alwaysSSL = true;
108
109  /**
110   * Called when this utility should perform its actions locally in this
111   * JVM.
112   *
113   * @param initializeServer indicates whether or not to initialize the
114   *        directory server in the case of a local action
115   * @param out stream to write messages; may be null
116   * @param err stream to write messages; may be null
117   * @return int indicating the result of this action
118   */
119  protected abstract int processLocal(boolean initializeServer,
120                                      PrintStream out,
121                                      PrintStream err);
122
123  /**
124   * Creates an argument parser prepopulated with arguments for processing
125   * input for scheduling tasks with the task backend.
126   *
127   * @param className of this tool
128   * @param toolDescription of this tool
129   * @return LDAPConnectionArgumentParser for processing CLI input
130   */
131  protected LDAPConnectionArgumentParser createArgParser(String className,
132    LocalizableMessage toolDescription)
133  {
134    ArgumentGroup ldapGroup = new ArgumentGroup(
135      INFO_DESCRIPTION_TASK_LDAP_ARGS.get(), 1001);
136
137    argParser = new LDAPConnectionArgumentParser(className,
138      toolDescription, false, ldapGroup, alwaysSSL);
139
140    ArgumentGroup taskGroup = new ArgumentGroup(
141      INFO_DESCRIPTION_TASK_TASK_ARGS.get(), 1000);
142
143    try {
144      StringArgument propertiesFileArgument =
145          CommonArguments.getPropertiesFile();
146      argParser.addArgument(propertiesFileArgument);
147      argParser.setFilePropertiesArgument(propertiesFileArgument);
148
149      BooleanArgument noPropertiesFileArgument =
150          CommonArguments.getNoPropertiesFile();
151      argParser.addArgument(noPropertiesFileArgument);
152      argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
153
154      taskScheduleArgs = new TaskScheduleArgs();
155
156      for (Argument arg : taskScheduleArgs.getArguments())
157      {
158        argParser.addArgument(arg, taskGroup);
159      }
160
161      testIfOfflineArg = new BooleanArgument("testIfOffline", null,
162          "testIfOffline", INFO_DESCRIPTION_TEST_IF_OFFLINE.get());
163      testIfOfflineArg.setHidden(true);
164      argParser.addArgument(testIfOfflineArg);
165    } catch (ArgumentException e) {
166      // should never happen
167    }
168
169    return argParser;
170  }
171
172  /**
173   * Validates arguments related to task scheduling.  This should be
174   * called after the <code>ArgumentParser.parseArguments</code> has
175   * been called.
176   *
177   * @throws ArgumentException if there is a problem with the arguments.
178   * @throws ClientException if there is a problem with one of the values provided
179   * by the user.
180   */
181  protected void validateTaskArgs() throws ArgumentException, ClientException
182  {
183    if (processAsTask())
184    {
185      taskScheduleArgs.validateArgs();
186    }
187    else
188    {
189      // server is offline => output logs to the console
190      JDKLogging.enableConsoleLoggingForOpenDJ(Level.FINE);
191      taskScheduleArgs.validateArgsIfOffline();
192    }
193  }
194
195  /** {@inheritDoc} */
196  @Override
197  public Date getStartDateTime() {
198    return taskScheduleArgs.getStartDateTime();
199  }
200
201  /** {@inheritDoc} */
202  @Override
203  public String getRecurringDateTime() {
204    return taskScheduleArgs.getRecurringDateTime();
205  }
206
207  /** {@inheritDoc} */
208  @Override
209  public List<String> getDependencyIds() {
210    return taskScheduleArgs.getDependencyIds();
211  }
212
213  /** {@inheritDoc} */
214  @Override
215  public FailedDependencyAction getFailedDependencyAction() {
216    return taskScheduleArgs.getFailedDependencyAction();
217  }
218
219  /** {@inheritDoc} */
220  @Override
221  public List<String> getNotifyUponCompletionEmailAddresses() {
222    return taskScheduleArgs.getNotifyUponCompletionEmailAddresses();
223  }
224
225  /** {@inheritDoc} */
226  @Override
227  public List<String> getNotifyUponErrorEmailAddresses() {
228    return taskScheduleArgs.getNotifyUponErrorEmailAddresses();
229  }
230
231  /**
232   * Either invokes initiates this tool's local action or schedule this
233   * tool using the tasks interface based on user input.
234   *
235   * @param argParser used to parse user arguments
236   * @param initializeServer indicates whether or not to initialize the
237   *        directory server in the case of a local action
238   * @param out stream to write messages; may be null
239   * @param err stream to write messages; may be null
240   * @return int indicating the result of this action
241   */
242  protected int process(LDAPConnectionArgumentParser argParser,
243                        boolean initializeServer,
244                        PrintStream out, PrintStream err) {
245    int ret;
246
247    if (testIfOffline())
248    {
249      if (!processAsTask())
250      {
251        return RUN_OFFLINE;
252      }
253      else
254      {
255        return RUN_ONLINE;
256      }
257    }
258
259    if (processAsTask())
260    {
261      if (initializeServer)
262      {
263        try
264        {
265          DirectoryServer.bootstrapClient();
266          DirectoryServer.initializeJMX();
267        }
268        catch (Exception e)
269        {
270          printWrappedText(err, ERR_SERVER_BOOTSTRAP_ERROR.get(getExceptionMessage(e)));
271          return 1;
272        }
273      }
274
275      LDAPConnection conn = null;
276      try {
277        conn = argParser.connect(out, err);
278        TaskClient tc = new TaskClient(conn);
279        TaskEntry taskEntry = tc.schedule(this);
280        LocalizableMessage startTime = taskEntry.getScheduledStartTime();
281        if (taskEntry.getTaskState() == TaskState.RECURRING) {
282          printWrappedText(out, INFO_TASK_TOOL_RECURRING_TASK_SCHEDULED.get(taskEntry.getType(), taskEntry.getId()));
283        } else if (startTime == null || startTime.length() == 0) {
284          printWrappedText(out, INFO_TASK_TOOL_TASK_SCHEDULED_NOW.get(taskEntry.getType(), taskEntry.getId()));
285        } else {
286          printWrappedText(out, INFO_TASK_TOOL_TASK_SCHEDULED_FUTURE.get(
287              taskEntry.getType(), taskEntry.getId(), taskEntry.getScheduledStartTime()));
288        }
289        if (!taskScheduleArgs.startArg.isPresent()) {
290
291          // Poll the task printing log messages until finished
292          String taskId = taskEntry.getId();
293          Set<LocalizableMessage> printedLogMessages = new HashSet<>();
294          do {
295            taskEntry = tc.getTaskEntry(taskId);
296            List<LocalizableMessage> logs = taskEntry.getLogMessages();
297            for (LocalizableMessage log : logs) {
298              if (printedLogMessages.add(log)) {
299                out.println(log);
300              }
301            }
302
303            try {
304              Thread.sleep(SYNCHRONOUS_TASK_POLL_INTERVAL);
305            } catch (InterruptedException e) {
306              // ignore
307            }
308
309          } while (!taskEntry.isDone());
310          if (TaskState.isSuccessful(taskEntry.getTaskState())) {
311            if (taskEntry.getTaskState() != TaskState.RECURRING) {
312              printWrappedText(out, INFO_TASK_TOOL_TASK_SUCESSFULL.get(taskEntry.getType(), taskEntry.getId()));
313            }
314            return 0;
315          } else {
316            printWrappedText(out, INFO_TASK_TOOL_TASK_NOT_SUCESSFULL.get(taskEntry.getType(), taskEntry.getId()));
317            return 1;
318          }
319        }
320        ret = 0;
321      } catch (LDAPConnectionException e) {
322        if (isWrongPortException(e,
323            Integer.valueOf(argParser.getArguments().getPort())))
324        {
325          printWrappedText(err, ERR_TASK_LDAP_FAILED_TO_CONNECT_WRONG_PORT.get(
326              argParser.getArguments().getHostName(), argParser.getArguments().getPort()));
327        } else {
328          printWrappedText(err, ERR_TASK_TOOL_START_TIME_NO_LDAP.get(e.getMessage()));
329        }
330        ret = 1;
331      } catch (DecodeException ae) {
332        printWrappedText(err, ERR_TASK_TOOL_DECODE_ERROR.get(ae.getMessage()));
333        ret = 1;
334      } catch (IOException ioe) {
335        printWrappedText(err, ERR_TASK_TOOL_IO_ERROR.get(ioe));
336        ret = 1;
337      } catch (LDAPException le) {
338        printWrappedText(err, ERR_TASK_TOOL_LDAP_ERROR.get(le.getMessage()));
339        ret = 1;
340      } catch (OpenDsException e) {
341        printWrappedText(err, e.getMessageObject());
342        ret = 1;
343      } catch (ArgumentException e) {
344        argParser.displayMessageAndUsageReference(err, e.getMessageObject());
345        ret = 1;
346      }
347      finally
348      {
349        if (conn != null)
350        {
351          try
352          {
353            conn.close(null);
354          }
355          catch (Throwable t)
356          {
357            // Ignore.
358          }
359        }
360      }
361    } else {
362      ret = processLocal(initializeServer, out, err);
363    }
364    return ret;
365  }
366
367  private boolean processAsTask() {
368    return argParser.connectionArgumentsPresent();
369  }
370
371  /**
372   * Returns {@code true} if the provided exception was caused by trying to
373   * connect to the wrong port and {@code false} otherwise.
374   * @param t the exception to be analyzed.
375   * @param port the port to which we tried to connect.
376   * @return {@code true} if the provided exception was caused by trying to
377   * connect to the wrong port and {@code false} otherwise.
378   */
379  private boolean isWrongPortException(Throwable t, int port)
380  {
381    boolean isWrongPortException = false;
382    boolean isDefaultClearPort = (port - 389) % 1000 == 0;
383    while (t != null && isDefaultClearPort)
384    {
385      isWrongPortException = t instanceof java.net.SocketTimeoutException;
386      if (!isWrongPortException)
387      {
388        t = t.getCause();
389      }
390      else
391      {
392        break;
393      }
394    }
395    return isWrongPortException;
396  }
397
398
399  /**
400   * Indicates whether we must return if the command must be run in off-line
401   * mode.
402   * @return <CODE>true</CODE> if we must return if the command must be run in
403   * off-line mode and <CODE>false</CODE> otherwise.
404   */
405  public boolean testIfOffline()
406  {
407    boolean returnValue = false;
408    if (testIfOfflineArg != null)
409    {
410      returnValue = testIfOfflineArg.isPresent();
411    }
412    return returnValue;
413  }
414
415  /**
416   * Checks that binary version and instance version are the same.
417   *
418   * @throws InitializationException
419   *           If versions mismatch
420   */
421  protected void checkVersion() throws InitializationException
422  {
423    // FIXME Do not perform this check if the tool is use in remote mode (see OPENDJ-1166)
424    BuildVersion.checkVersionMismatch();
425  }
426}