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