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 *      Portions Copyright 2013-2016 ForgeRock AS
025 */
026package org.opends.server.tools.upgrade;
027
028import static javax.security.auth.callback.ConfirmationCallback.NO;
029import static javax.security.auth.callback.ConfirmationCallback.YES;
030import static javax.security.auth.callback.TextOutputCallback.*;
031
032import static org.opends.messages.ToolMessages.*;
033import static org.opends.server.tools.upgrade.FileManager.copy;
034import static org.opends.server.tools.upgrade.Installation.CURRENT_CONFIG_FILE_NAME;
035import static org.opends.server.tools.upgrade.UpgradeUtils.*;
036import static org.opends.server.util.StaticUtils.isClassAvailable;
037
038import java.io.File;
039import java.io.IOException;
040import java.util.Arrays;
041import java.util.HashMap;
042import java.util.HashSet;
043import java.util.LinkedList;
044import java.util.List;
045import java.util.Map;
046import java.util.Set;
047import java.util.TreeSet;
048
049import javax.security.auth.callback.TextOutputCallback;
050
051import org.forgerock.i18n.LocalizableMessage;
052import org.forgerock.i18n.slf4j.LocalizedLogger;
053import org.forgerock.opendj.ldap.DN;
054import org.forgerock.opendj.ldap.Entry;
055import org.forgerock.opendj.ldap.Filter;
056import org.forgerock.opendj.ldap.SearchScope;
057import org.forgerock.opendj.ldap.requests.Requests;
058import org.forgerock.opendj.ldap.requests.SearchRequest;
059import org.forgerock.opendj.ldif.EntryReader;
060import org.forgerock.util.Utils;
061import org.opends.server.backends.pluggable.spi.TreeName;
062import org.opends.server.tools.JavaPropertiesTool;
063import org.opends.server.tools.RebuildIndex;
064import org.opends.server.util.BuildVersion;
065import org.opends.server.util.ChangeOperationType;
066import org.opends.server.util.StaticUtils;
067
068import com.forgerock.opendj.cli.ClientException;
069import com.forgerock.opendj.cli.ReturnCode;
070import com.sleepycat.je.DatabaseException;
071import com.sleepycat.je.Environment;
072import com.sleepycat.je.EnvironmentConfig;
073import com.sleepycat.je.Transaction;
074import com.sleepycat.je.TransactionConfig;
075
076/** Factory methods for create new upgrade tasks. */
077public final class UpgradeTasks
078{
079  /** Logger for the upgrade. */
080  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
081
082  /** An errors counter in case of ignore errors mode. */
083  static int countErrors;
084
085  /** Contains all the indexes to rebuild. */
086  static Set<String> indexesToRebuild = new HashSet<>();
087
088  /** A flag to avoid rebuild single indexes if 'rebuild all' is selected. */
089  static boolean isRebuildAllIndexesIsPresent;
090
091  /** A flag for marking 'rebuild all' task accepted by user. */
092  static boolean isRebuildAllIndexesTaskAccepted;
093
094  /**
095   * Returns a new upgrade task which adds a config entry to the underlying
096   * config file.
097   *
098   * @param summary
099   *          The summary of this upgrade task.
100   * @param ldif
101   *          The LDIF record which will be applied to matching entries.
102   * @return A new upgrade task which applies an LDIF record to all
103   *         configuration entries matching the provided filter.
104   */
105  public static UpgradeTask addConfigEntry(final LocalizableMessage summary,
106      final String... ldif)
107  {
108    return updateConfigEntry(summary, null, ChangeOperationType.ADD, ldif);
109  }
110
111  /**
112   * This task copies the file placed in parameter within the config / schema
113   * folder. If the file already exists, it's overwritten.
114   *
115   * @param fileName
116   *          The name of the file which need to be copied.
117   * @return A task which copy the the file placed in parameter within the
118   *         config / schema folder. If the file already exists, it's
119   *         overwritten.
120   */
121  public static UpgradeTask copySchemaFile(final String fileName)
122  {
123    return new AbstractUpgradeTask()
124    {
125      @Override
126      public void perform(final UpgradeContext context) throws ClientException
127      {
128        final LocalizableMessage msg = INFO_UPGRADE_TASK_REPLACE_SCHEMA_FILE.get(fileName);
129        logger.debug(msg);
130
131        final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
132
133        final File schemaFileTemplate =
134            new File(templateConfigSchemaDirectory, fileName);
135
136        try
137        {
138          context.notifyProgress(pnc.setProgress(20));
139          if (!schemaFileTemplate.exists() || schemaFileTemplate.length() == 0)
140          {
141            throw new IOException(ERR_UPGRADE_CORRUPTED_TEMPLATE
142                .get(schemaFileTemplate.getPath()).toString());
143          }
144          copy(schemaFileTemplate, configSchemaDirectory, true);
145          context.notifyProgress(pnc.setProgress(100));
146        }
147        catch (final IOException e)
148        {
149          manageTaskException(context, ERR_UPGRADE_COPYSCHEMA_FAILS.get(
150              schemaFileTemplate.getName(), e.getMessage()), pnc);
151        }
152      }
153
154      @Override
155      public String toString()
156      {
157        return INFO_UPGRADE_TASK_REPLACE_SCHEMA_FILE.get(fileName).toString();
158      }
159    };
160  }
161
162  /**
163   * This task copies the file placed in parameter within the config folder. If
164   * the file already exists, it's overwritten.
165   *
166   * @param fileName
167   *          The name of the file which need to be copied.
168   * @return A task which copy the the file placed in parameter within the
169   *         config folder. If the file already exists, it's overwritten.
170   */
171  public static UpgradeTask addConfigFile(final String fileName)
172  {
173    return new AbstractUpgradeTask()
174    {
175      @Override
176      public void perform(final UpgradeContext context) throws ClientException
177      {
178        final LocalizableMessage msg = INFO_UPGRADE_TASK_ADD_CONFIG_FILE.get(fileName);
179        logger.debug(msg);
180
181        final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
182
183        final File configFile = new File(templateConfigDirectory, fileName);
184
185        try
186        {
187          context.notifyProgress(pnc.setProgress(20));
188
189          copy(configFile, configDirectory, true);
190          context.notifyProgress(pnc.setProgress(100));
191        }
192        catch (final IOException e)
193        {
194          manageTaskException(context, ERR_UPGRADE_ADD_CONFIG_FILE_FAILS.get(
195              configFile.getName(), e.getMessage()), pnc);
196        }
197      }
198
199      @Override
200      public String toString()
201      {
202        return INFO_UPGRADE_TASK_ADD_CONFIG_FILE.get(fileName).toString();
203      }
204    };
205  }
206
207  /**
208   * Returns a new upgrade task which deletes a config entry from the underlying
209   * config file.
210   *
211   * @param summary
212   *          The summary of this upgrade task.
213   * @param dnInLDIF
214   *          The dn to delete in the form of LDIF.
215   * @return A new upgrade task which applies an LDIF record to all
216   *         configuration entries matching the provided filter.
217   */
218  public static UpgradeTask deleteConfigEntry(final LocalizableMessage summary,
219      final String dnInLDIF)
220  {
221    return updateConfigEntry(summary, null, ChangeOperationType.DELETE, dnInLDIF);
222  }
223
224  /**
225   * Returns a new upgrade task which applies an LDIF record to all
226   * configuration entries matching the provided filter.
227   *
228   * @param summary
229   *          The summary of this upgrade task.
230   * @param filter
231   *          The LDAP filter which configuration entries must match.
232   * @param ldif
233   *          The LDIF record which will be applied to matching entries.
234   * @return A new upgrade task which applies an LDIF record to all
235   *         configuration entries matching the provided filter.
236   */
237  public static UpgradeTask modifyConfigEntry(final LocalizableMessage summary,
238      final String filter, final String... ldif)
239  {
240    return updateConfigEntry(summary, filter, ChangeOperationType.MODIFY, ldif);
241  }
242
243  /**
244   * This task adds a new attribute type (must exists in the original file) to
245   * the specified file placed in parameter. The destination must be a file
246   * contained in the config/schema folder. E.g : This example adds a new
247   * attribute type named 'etag' in the 00.core.ldif. The 'etag' attribute
248   * already exists in the 00-core.ldif template schema file.
249   *
250   * <pre>
251   * register(&quot;2.5.0.7192&quot;,
252   *   newAttributeTypes(LocalizableMessage.raw(&quot;New attribute etag&quot;),
253   *   false, &quot;00-core.ldif&quot;,
254   *   &quot;1.3.6.1.4.1.36733.2.1.1.59&quot;));
255   * </pre>
256   *
257   * @param summary
258   *          The summary of the task.
259   * @param fileName
260   *          The file where to add the new attribute types. This file must be
261   *          contained in the configuration/schema folder.
262   * @param attributeOids
263   *          The OIDs of the new attributes to add to.
264   * @return An upgrade task which adds new attribute types, defined previously
265   *         in the configuration template files, reads the definition
266   *         and adds it onto the specified file in parameter.
267   */
268  public static UpgradeTask newAttributeTypes(final LocalizableMessage summary,
269      final String fileName, final String... attributeOids)
270  {
271    return new AbstractUpgradeTask()
272    {
273      @Override
274      public void perform(final UpgradeContext context) throws ClientException
275      {
276        logger.debug(summary);
277
278        final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20);
279        context.notifyProgress(pnc);
280
281        final File schemaFileTemplate =
282            new File(templateConfigSchemaDirectory, fileName);
283
284        final File pathDestination = new File(configSchemaDirectory, fileName);
285        try
286        {
287          final int changeCount =
288              updateSchemaFile(schemaFileTemplate, pathDestination,
289                  attributeOids, null);
290
291          displayChangeCount(pathDestination.getPath(), changeCount);
292
293          context.notifyProgress(pnc.setProgress(100));
294        }
295        catch (final IOException | IllegalStateException e)
296        {
297          manageTaskException(context, ERR_UPGRADE_ADDATTRIBUTE_FAILS.get(
298              schemaFileTemplate.getName(), e.getMessage()), pnc);
299        }
300      }
301
302      @Override
303      public String toString()
304      {
305        return String.valueOf(summary);
306      }
307    };
308  }
309
310  /**
311   * This task adds a new object class (must exists in the original file) to the
312   * specified file placed in parameter. The destination must be a file
313   * contained in the config/schema folder.
314   *
315   * @param summary
316   *          The summary of the task.
317   * @param fileName
318   *          The file where to add the new object classes. This file must be
319   *          contained in the configuration/schema folder.
320   * @param objectClassesOids
321   *          The OIDs of the new object classes to add to.
322   * @return An upgrade task which adds new object classes, defined previously
323   *         in the configuration template files,
324   *         reads the definition and adds it onto the specified file in
325   *         parameter.
326   */
327  public static UpgradeTask newObjectClasses(final LocalizableMessage summary,
328      final String fileName, final String... objectClassesOids)
329  {
330    return new AbstractUpgradeTask()
331    {
332      @Override
333      public void perform(final UpgradeContext context) throws ClientException
334      {
335        logger.debug(summary);
336
337        final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20);
338        context.notifyProgress(pnc);
339
340        final File schemaFileTemplate =
341            new File(templateConfigSchemaDirectory, fileName);
342
343        final File pathDestination = new File(configSchemaDirectory, fileName);
344
345        context.notifyProgress(pnc.setProgress(20));
346
347        try
348        {
349          final int changeCount =
350              updateSchemaFile(schemaFileTemplate, pathDestination,
351                  null, objectClassesOids);
352
353          displayChangeCount(pathDestination.getPath(), changeCount);
354
355          context.notifyProgress(pnc.setProgress(100));
356        }
357        catch (final IOException e)
358        {
359          manageTaskException(context, ERR_UPGRADE_ADDOBJECTCLASS_FAILS.get(
360              schemaFileTemplate.getName(), e.getMessage()), pnc);
361        }
362        catch (final IllegalStateException e)
363        {
364          manageTaskException(context, ERR_UPGRADE_ADDATTRIBUTE_FAILS.get(
365              schemaFileTemplate.getName(), e.getMessage()), pnc);
366        }
367      }
368
369      @Override
370      public String toString()
371      {
372        return String.valueOf(summary);
373      }
374    };
375  }
376
377  /**
378   * Re-run the dsjavaproperties tool to rewrite the set-java-home script/batch file.
379   *
380   * @param summary
381   *          The summary of the task.
382   * @return An upgrade task which runs dsjavaproperties.
383   */
384  public static UpgradeTask rerunJavaPropertiesTool(final LocalizableMessage summary)
385  {
386    return new AbstractUpgradeTask()
387    {
388      @Override
389      public void perform(UpgradeContext context) throws ClientException
390      {
391        logger.debug(summary);
392
393        final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 50);
394        context.notifyProgress(pnc);
395
396        int returnValue = JavaPropertiesTool.mainCLI("--quiet");
397        context.notifyProgress(pnc.setProgress(100));
398
399        if (JavaPropertiesTool.ErrorReturnCode.SUCCESSFUL.getReturnCode() != returnValue &&
400                JavaPropertiesTool.ErrorReturnCode.SUCCESSFUL_NOP.getReturnCode() != returnValue) {
401          throw new ClientException(ReturnCode.ERROR_UNEXPECTED, ERR_UPGRADE_DSJAVAPROPERTIES_FAILED.get());
402        }
403      }
404
405      @Override
406      public String toString()
407      {
408        return String.valueOf(summary);
409      }
410    };
411  }
412
413  /**
414   * Creates a group of tasks which will only be invoked if the current version
415   * is more recent than the provided version. This may be useful in cases where
416   * a regression was introduced in version X and resolved in a later version Y.
417   * In this case, the provided upgrade tasks will only be invoked if the
418   * current version is between X (inclusive) and Y (exclusive).
419   *
420   * @param versionString
421   *          The lower bound version. The upgrade tasks will not be applied if
422   *          the current version is older than this version.
423   * @param tasks
424   *          The group of tasks to invoke if the current version is equal to or
425   *          more recent than {@code versionString}.
426   * @return An upgrade task which will only be invoked if the current version
427   *         is more recent than the provided version.
428   */
429  public static UpgradeTask regressionInVersion(final String versionString, final UpgradeTask... tasks)
430  {
431    final BuildVersion version = BuildVersion.valueOf(versionString);
432    return conditionalUpgradeTasks(new UpgradeCondition()
433    {
434      @Override
435      public boolean shouldPerformUpgradeTasks(final UpgradeContext context) throws ClientException
436      {
437        return context.getFromVersion().compareTo(version) >= 0;
438      }
439
440      @Override
441      public String toString()
442      {
443        return "Regression in version \"" + versionString + "\"";
444      }
445    }, tasks);
446  }
447
448  /**
449   * Creates a group of tasks which will only be invoked if the user confirms agreement. This may be
450   * useful in cases where a feature is deprecated and the upgrade is capable of migrating the
451   * configuration to the new replacement feature.
452   *
453   * @param message
454   *          The confirmation message.
455   * @param tasks
456   *          The group of tasks to invoke if the user agrees.
457   * @return An upgrade task which will only be invoked if the user confirms agreement.
458   */
459  static UpgradeTask requireConfirmation(
460          final LocalizableMessage message, final int defaultResponse, final UpgradeTask... tasks)
461  {
462    return conditionalUpgradeTasks(new UpgradeCondition()
463    {
464      @Override
465      public boolean shouldPerformUpgradeTasks(final UpgradeContext context) throws ClientException
466      {
467        return context.confirmYN(INFO_UPGRADE_TASK_NEEDS_USER_CONFIRM.get(message), defaultResponse) == YES;
468      }
469
470      @Override
471      public String toString()
472      {
473        return INFO_UPGRADE_TASK_NEEDS_USER_CONFIRM.get(message).toString();
474      }
475    }, tasks);
476  }
477
478  /** Determines whether conditional tasks should be performed. */
479  interface UpgradeCondition
480  {
481    boolean shouldPerformUpgradeTasks(UpgradeContext context) throws ClientException;
482  }
483
484  static UpgradeTask conditionalUpgradeTasks(final UpgradeCondition condition, final UpgradeTask... tasks)
485  {
486    return new AbstractUpgradeTask()
487    {
488      private boolean shouldPerformUpgradeTasks = true;
489
490      @Override
491      public void prepare(final UpgradeContext context) throws ClientException
492      {
493        shouldPerformUpgradeTasks = condition.shouldPerformUpgradeTasks(context);
494        if (shouldPerformUpgradeTasks)
495        {
496          for (UpgradeTask task : tasks)
497          {
498            task.prepare(context);
499          }
500        }
501      }
502
503      @Override
504      public void perform(final UpgradeContext context) throws ClientException
505      {
506        if (shouldPerformUpgradeTasks)
507        {
508          for (UpgradeTask task : tasks)
509          {
510            task.perform(context);
511          }
512        }
513      }
514
515      @Override
516      public void postUpgrade(UpgradeContext context) throws ClientException
517      {
518        if (shouldPerformUpgradeTasks)
519        {
520          boolean isOk = true;
521          for (final UpgradeTask task : tasks)
522          {
523            if (isOk)
524            {
525              try
526              {
527                task.postUpgrade(context);
528              }
529              catch (ClientException e)
530              {
531                logger.error(LocalizableMessage.raw(e.getMessage()));
532                isOk = false;
533              }
534            }
535            else
536            {
537              task.postponePostUpgrade(context);
538            }
539          }
540        }
541      }
542
543      @Override
544      public String toString()
545      {
546        final StringBuilder sb = new StringBuilder();
547        sb.append(condition).append(" = ").append(shouldPerformUpgradeTasks).append('\n');
548        sb.append('[');
549        Utils.joinAsString(sb, "\n", (Object[]) tasks);
550        sb.append(']');
551        return sb.toString();
552      }
553    };
554  }
555
556  /**
557   * Creates a rebuild all indexes task.
558   *
559   * @param summary
560   *          The summary of this upgrade task.
561   * @return An Upgrade task which rebuild all the indexes.
562   */
563  public static UpgradeTask rebuildAllIndexes(final LocalizableMessage summary)
564  {
565    return new AbstractUpgradeTask()
566    {
567      private boolean isATaskToPerform;
568
569      @Override
570      public void prepare(UpgradeContext context) throws ClientException
571      {
572        Upgrade.setHasPostUpgradeTask(true);
573        // Requires answer from the user.
574        isATaskToPerform = context.confirmYN(summary, NO) == YES;
575        isRebuildAllIndexesIsPresent = true;
576        isRebuildAllIndexesTaskAccepted = isATaskToPerform;
577      }
578
579      @Override
580      public void postUpgrade(final UpgradeContext context) throws ClientException
581      {
582        if (!isATaskToPerform)
583        {
584          postponePostUpgrade(context);
585        }
586      }
587
588      @Override
589      public void postponePostUpgrade(UpgradeContext context) throws ClientException
590      {
591        context.notify(INFO_UPGRADE_ALL_REBUILD_INDEX_DECLINED.get(), TextOutputCallback.WARNING);
592      }
593
594      @Override
595      public String toString()
596      {
597        return String.valueOf(summary);
598      }
599    };
600  }
601
602  /**
603   * Creates a rebuild index task for a given single index. As this task is
604   * possibly lengthy, it's considered as a post upgrade task. This task is not
605   * mandatory; e.g not require user interaction, but could be required to get a
606   * fully functional server. <br />
607   * The post upgrade task just register the task. The rebuild indexes tasks are
608   * completed at the end of the upgrade process.
609   *
610   * @param summary
611   *          A message describing why the index needs to be rebuilt and asking
612   *          them whether or not they wish to perform this task after the
613   *          upgrade.
614   * @param index
615   *          The index to rebuild.
616   * @return The rebuild index task.
617   */
618  public static UpgradeTask rebuildSingleIndex(final LocalizableMessage summary,
619      final String index)
620  {
621    return new AbstractUpgradeTask()
622    {
623      private boolean isATaskToPerform;
624
625      @Override
626      public void prepare(UpgradeContext context) throws ClientException
627      {
628        Upgrade.setHasPostUpgradeTask(true);
629        // Requires answer from the user.
630        isATaskToPerform = context.confirmYN(summary, NO) == YES;
631      }
632
633      @Override
634      public void postUpgrade(final UpgradeContext context) throws ClientException
635      {
636        if (isATaskToPerform)
637        {
638          indexesToRebuild.add(index);
639        }
640        else
641        {
642          postponePostUpgrade(context);
643        }
644      }
645
646      @Override
647      public void postponePostUpgrade(UpgradeContext context) throws ClientException
648      {
649        if (!isRebuildAllIndexesIsPresent)
650        {
651          context.notify(INFO_UPGRADE_REBUILD_INDEX_DECLINED.get(index), TextOutputCallback.WARNING);
652        }
653      }
654
655      @Override
656      public String toString()
657      {
658        return String.valueOf(summary);
659      }
660    };
661  }
662
663  /**
664   * This task is processed at the end of the upgrade, rebuilding indexes. If a
665   * rebuild all indexes has been registered before, it takes the flag
666   * relatively to single rebuild index.
667   *
668   * @return The post upgrade rebuild indexes task.
669   */
670  public static UpgradeTask postUpgradeRebuildIndexes()
671  {
672    return new AbstractUpgradeTask()
673    {
674      @Override
675      public void postUpgrade(final UpgradeContext context) throws ClientException
676      {
677        LocalizableMessage message = null;
678        final List<String> args = new LinkedList<>();
679
680        if (isRebuildAllIndexesIsPresent && isRebuildAllIndexesTaskAccepted)
681        {
682          args.add("--rebuildAll");
683          message = INFO_UPGRADE_REBUILD_ALL.get();
684        }
685        else if (!indexesToRebuild.isEmpty()
686            && !isRebuildAllIndexesTaskAccepted)
687        {
688          message = INFO_UPGRADE_REBUILD_INDEX_STARTS.get(indexesToRebuild);
689
690          // Adding all requested indexes.
691          for (final String indexToRebuild : indexesToRebuild)
692          {
693            args.add("-i");
694            args.add(indexToRebuild);
695          }
696        }
697        else
698        {
699          return;
700        }
701        // Startup message.
702        ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, message, 25);
703        logger.debug(message);
704        context.notifyProgress(pnc);
705
706        // Sets the arguments like the rebuild index command line.
707        args.addAll(Arrays.asList(
708            "-f",
709            new File(configDirectory, CURRENT_CONFIG_FILE_NAME).getAbsolutePath()));
710
711        /*
712         * Index(es) could be contained in several backends or none, If none,
713         * the post upgrade tasks succeed and a message is printed in the
714         * upgrade log file.
715         */
716        final List<String> backends = UpgradeUtils.getIndexedBackendsFromConfig();
717        if (backends.isEmpty())
718        {
719          logger.debug(INFO_UPGRADE_REBUILD_INDEX_NO_BACKEND_FOUND);
720          logger.debug(INFO_UPGRADE_REBUILD_INDEX_DECLINED, indexesToRebuild);
721          context.notifyProgress(pnc.setProgress(100));
722          return;
723        }
724
725        for (final String be : backends)
726        {
727          args.add("-b");
728          args.add(be);
729        }
730
731        // Displays info about command line args for log only.
732        logger.debug(INFO_UPGRADE_REBUILD_INDEX_ARGUMENTS, args);
733
734        /*
735         * The rebuild-index process just display a status ok / fails. The
736         * logger stream contains all the log linked to this process. The
737         * complete process is not displayed in the upgrade console.
738         */
739        final String[] commandLineArgs = args.toArray(new String[args.size()]);
740        final int result = new RebuildIndex().rebuildIndexesWithinMultipleBackends(
741            true, UpgradeLog.getPrintStream(), commandLineArgs);
742
743        if (result == 0)
744        {
745          logger.debug(INFO_UPGRADE_REBUILD_INDEX_ENDS);
746          context.notifyProgress(pnc.setProgress(100));
747        }
748        else
749        {
750          final LocalizableMessage msg = ERR_UPGRADE_PERFORMING_POST_TASKS_FAIL.get();
751          context.notifyProgress(pnc.setProgress(-100));
752          throw new ClientException(ReturnCode.ERROR_UNEXPECTED, msg);
753        }
754      }
755
756      @Override
757      public String toString()
758      {
759        return "Post upgrade rebuild indexes task";
760      }
761    };
762  }
763
764  /**
765   * Creates a file object representing config/upgrade/schema.ldif.current which
766   * the server creates the first time it starts if there are schema
767   * customizations.
768   *
769   * @return An upgrade task which upgrade the config/upgrade folder, creating a
770   *         new schema.ldif.rev which is needed after schema customization for
771   *         starting correctly the server.
772   */
773  public static UpgradeTask updateConfigUpgradeFolder()
774  {
775    return new AbstractUpgradeTask()
776    {
777      @Override
778      public void perform(final UpgradeContext context) throws ClientException
779      {
780        final LocalizableMessage msg = INFO_UPGRADE_TASK_REFRESH_UPGRADE_DIRECTORY.get();
781        logger.debug(msg);
782
783        final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 20);
784        context.notifyProgress(pnc);
785
786        try
787        {
788          String toRevision = context.getToVersion().getRevision();
789          updateConfigUpgradeSchemaFile(configSchemaDirectory, toRevision);
790
791          context.notifyProgress(pnc.setProgress(100));
792        }
793        catch (final Exception ex)
794        {
795          manageTaskException(context, ERR_UPGRADE_CONFIG_ERROR_UPGRADE_FOLDER.get(ex.getMessage()), pnc);
796        }
797      }
798
799      @Override
800      public String toString()
801      {
802        return INFO_UPGRADE_TASK_REFRESH_UPGRADE_DIRECTORY.get().toString();
803      }
804    };
805  }
806
807  /**
808   * Renames the SNMP security config file if it exists. Since 2.5.0.7466 this
809   * file has been renamed.
810   *
811   * @param summary
812   *          The summary of this upgrade task.
813   * @return An upgrade task which renames the old SNMP security config file if
814   *         it exists.
815   */
816  public static UpgradeTask renameSnmpSecurityConfig(final LocalizableMessage summary)
817  {
818    return new AbstractUpgradeTask()
819    {
820      @Override
821      public void perform(final UpgradeContext context) throws ClientException
822      {
823        /*
824         * Snmp config file contains old name in old version(like 2.4.5), in
825         * order to make sure the process will still work after upgrade, we need
826         * to rename it - only if it exists.
827         */
828        final File snmpDir = UpgradeUtils.configSnmpSecurityDirectory;
829        if (snmpDir.exists())
830        {
831          ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 0);
832          try
833          {
834            final File oldSnmpConfig = new File(snmpDir, "opends-snmp.security");
835            if (oldSnmpConfig.exists())
836            {
837              context.notifyProgress(pnc.setProgress(20));
838              logger.debug(summary);
839
840              final File snmpConfig = new File(snmpDir, "opendj-snmp.security");
841              FileManager.rename(oldSnmpConfig, snmpConfig);
842
843              context.notifyProgress(pnc.setProgress(100));
844            }
845          }
846          catch (final Exception ex)
847          {
848            LocalizableMessage msg = ERR_UPGRADE_RENAME_SNMP_SECURITY_CONFIG_FILE.get(ex.getMessage());
849            manageTaskException(context, msg, pnc);
850          }
851        }
852      }
853
854      @Override
855      public String toString()
856      {
857        return String.valueOf(summary);
858      }
859    };
860  }
861
862  /**
863   * Removes the specified file from the file-system.
864   *
865   * @param file
866   *          The file to be removed.
867   * @return An upgrade task which removes the specified file from the file-system.
868   */
869  public static UpgradeTask deleteFile(final File file)
870  {
871    return new AbstractUpgradeTask()
872    {
873      @Override
874      public void perform(UpgradeContext context) throws ClientException
875      {
876        LocalizableMessage msg = INFO_UPGRADE_TASK_DELETE_FILE.get(file);
877        ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
878        context.notifyProgress(pnc);
879        try
880        {
881          FileManager.deleteRecursively(file);
882          context.notifyProgress(pnc.setProgress(100));
883        }
884        catch (Exception e)
885        {
886          manageTaskException(context, LocalizableMessage.raw(e.getMessage()), pnc);
887        }
888      }
889
890      @Override
891      public String toString()
892      {
893        return INFO_UPGRADE_TASK_DELETE_FILE.get(file).toString();
894      }
895    };
896  }
897
898  /**
899   * Creates an upgrade task which is responsible for preparing local-db backend JE databases for a full rebuild once
900   * they have been converted to pluggable JE backends.
901   *
902   * @return An upgrade task which is responsible for preparing local-db backend JE databases.
903   */
904  public static UpgradeTask migrateLocalDBBackendsToJEBackends() {
905    return new AbstractUpgradeTask() {
906      /** Properties of JE backends to be migrated. */
907      class Backend {
908        final String id;
909        final boolean isEnabled;
910        final Set<DN> baseDNs;
911        final File envDir;
912        final Map<String, String> renamedDbs = new HashMap<>();
913
914        private Backend(Entry config) {
915          id = config.parseAttribute("ds-cfg-backend-id").asString();
916          isEnabled = config.parseAttribute("ds-cfg-enabled").asBoolean(false);
917          baseDNs = config.parseAttribute("ds-cfg-base-dn").asSetOfDN();
918          String dbDirectory = config.parseAttribute("ds-cfg-db-directory").asString();
919          File backendParentDirectory = new File(dbDirectory);
920          if (!backendParentDirectory.isAbsolute()) {
921            backendParentDirectory = new File(getInstancePath(), dbDirectory);
922          }
923          envDir = new File(backendParentDirectory, id);
924          for (String db : Arrays.asList("compressed_attributes", "compressed_object_classes")) {
925            renamedDbs.put(db, new TreeName("compressed_schema", db).toString());
926          }
927          for (DN baseDN : baseDNs) {
928            renamedDbs.put(oldName(baseDN), newName(baseDN));
929          }
930        }
931      }
932
933      private final List<Backend> backends = new LinkedList<>();
934
935      /**
936       * Finds all the existing JE backends and determines if they can be migrated or not. It will not be possible to
937       * migrate a JE backend if the id2entry database name cannot easily be determined, which may happen because
938       * matching rules have changed significantly in 3.0.0.
939       */
940      @Override
941      public void prepare(final UpgradeContext context) throws ClientException {
942        // Requires answer from the user.
943        if (context.confirmYN(INFO_UPGRADE_TASK_MIGRATE_JE_DESCRIPTION.get(), NO) != YES) {
944          throw new ClientException(ReturnCode.ERROR_USER_CANCELLED,
945                                    INFO_UPGRADE_TASK_MIGRATE_JE_CANCELLED.get());
946        }
947
948        final SearchRequest sr = Requests.newSearchRequest("", SearchScope.WHOLE_SUBTREE,
949                                                           "(objectclass=ds-cfg-local-db-backend)");
950        try (final EntryReader entryReader = searchConfigFile(sr)) {
951          // Abort the upgrade if there are JE backends but no JE library.
952          if (entryReader.hasNext() && !isJeLibraryAvailable()) {
953            throw new ClientException(ReturnCode.CONSTRAINT_VIOLATION, INFO_UPGRADE_TASK_MIGRATE_JE_NO_JE_LIB.get());
954          }
955          while (entryReader.hasNext()) {
956            Backend backend = new Backend(entryReader.readEntry());
957            if (backend.isEnabled) {
958              abortIfBackendCannotBeMigrated(backend);
959            }
960            backends.add(backend);
961          }
962        } catch (IOException e) {
963          throw new ClientException(ReturnCode.APPLICATION_ERROR, INFO_UPGRADE_TASK_MIGRATE_CONFIG_READ_FAIL.get(), e);
964        }
965      }
966
967      private void abortIfBackendCannotBeMigrated(final Backend backend) throws ClientException {
968        Set<String> existingDatabases = JEHelper.listDatabases(backend.envDir);
969        for (DN baseDN : backend.baseDNs) {
970          final String oldName = oldName(baseDN);
971          if (!existingDatabases.contains(oldName)) {
972            LocalizableMessage msg = INFO_UPGRADE_TASK_MIGRATE_JE_UGLY_DN.get(backend.id, baseDN);
973            throw new ClientException(ReturnCode.CONSTRAINT_VIOLATION, msg);
974          }
975        }
976      }
977
978      /**
979       * Renames the compressed schema indexes and id2entry in a 2.x environment to
980       * the naming scheme used in 3.0.0. Before 3.0.0 JE databases were named as follows:
981       *
982       * 1) normalize the base DN
983       * 2) replace all non-alphanumeric characters with '_'
984       * 3) append '_'
985       * 4) append the index name.
986       *
987       * For example, id2entry in the base DN dc=white space,dc=com would be named
988       * dc_white_space_dc_com_id2entry. In 3.0.0 JE databases are named as follows:
989       *
990       * 1) normalize the base DN and URL encode it (' '  are converted to %20)
991       * 2) format as '/' + URL encoded base DN + '/' + index name.
992       *
993       * The matching rules in 3.0.0 are not compatible with previous versions, so we need
994       * to do a best effort attempt to figure out the old database name from a given base DN.
995       */
996      @Override
997      public void perform(final UpgradeContext context) throws ClientException {
998        if (!isJeLibraryAvailable()) {
999          return;
1000        }
1001
1002        for (Backend backend : backends) {
1003          if (backend.isEnabled) {
1004            ProgressNotificationCallback pnc = new ProgressNotificationCallback(
1005                    INFORMATION, INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_1.get(backend.id), 0);
1006            context.notifyProgress(pnc);
1007            try {
1008              JEHelper.migrateDatabases(backend.envDir, backend.renamedDbs);
1009              context.notifyProgress(pnc.setProgress(100));
1010            } catch (ClientException e) {
1011              manageTaskException(context, e.getMessageObject(), pnc);
1012            }
1013          } else {
1014            // Skip backends which have been disabled.
1015            final ProgressNotificationCallback pnc = new ProgressNotificationCallback(
1016                    INFORMATION, INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_5.get(backend.id), 0);
1017            context.notifyProgress(pnc);
1018            context.notifyProgress(pnc.setProgress(100));
1019          }
1020        }
1021      }
1022
1023      private boolean isJeLibraryAvailable() {
1024        return isClassAvailable("com.sleepycat.je.Environment");
1025      }
1026
1027      private String newName(final DN baseDN) {
1028        return new TreeName(baseDN.toNormalizedUrlSafeString(), "id2entry").toString();
1029      }
1030
1031      private String oldName(final DN baseDN) {
1032        String s = baseDN.toString();
1033        StringBuilder builder = new StringBuilder();
1034        for (int i = 0; i < s.length(); i++) {
1035          char c = s.charAt(i);
1036          builder.append(Character.isLetterOrDigit(c) ? c : '_');
1037        }
1038        builder.append("_id2entry");
1039        return builder.toString();
1040      }
1041
1042      @Override
1043      public String toString()
1044      {
1045        return INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_1.get("%s").toString();
1046      }
1047    };
1048  }
1049
1050  /**
1051   * Creates backups of the local DB backends directories by renaming adding them a ".bak" suffix.
1052   *  e.g "userRoot" would become "userRoot.bak"
1053   */
1054  static UpgradeTask renameLocalDBBackendDirectories()
1055  {
1056    return new AbstractUpgradeTask()
1057    {
1058      private boolean reimportRequired;
1059
1060      @Override
1061      public void perform(UpgradeContext context) throws ClientException
1062      {
1063        try
1064        {
1065          Filter filter = Filter.equality("objectclass", "ds-cfg-local-db-backend");
1066          SearchRequest findLocalDBBackends = Requests.newSearchRequest(DN.rootDN(), SearchScope.WHOLE_SUBTREE, filter);
1067          try (final EntryReader jeBackends = searchConfigFile(findLocalDBBackends))
1068          {
1069            while (jeBackends.hasNext())
1070            {
1071              Upgrade.setHasPostUpgradeTask(true);
1072              reimportRequired = true;
1073
1074              Entry jeBackend = jeBackends.readEntry();
1075              File dbParent = UpgradeUtils.getFileForPath(jeBackend.parseAttribute("ds-cfg-db-directory").asString());
1076              String id = jeBackend.parseAttribute("ds-cfg-backend-id").asString();
1077
1078              // Use canonical paths so that the progress message is more readable.
1079              File dbDirectory = new File(dbParent, id).getCanonicalFile();
1080              File dbDirectoryBackup = new File(dbParent, id + ".bak").getCanonicalFile();
1081              if (dbDirectory.exists() && !dbDirectoryBackup.exists())
1082              {
1083                LocalizableMessage msg = INFO_UPGRADE_TASK_RENAME_JE_DB_DIR.get(dbDirectory, dbDirectoryBackup);
1084                ProgressNotificationCallback pnc = new ProgressNotificationCallback(0, msg, 0);
1085                context.notifyProgress(pnc);
1086                boolean renameSucceeded = dbDirectory.renameTo(dbDirectoryBackup);
1087                context.notifyProgress(pnc.setProgress(renameSucceeded ? 100 : -1));
1088              }
1089            }
1090          }
1091        }
1092        catch (Exception e)
1093        {
1094          logger.error(LocalizableMessage.raw(e.getMessage()));
1095        }
1096      }
1097
1098      @Override
1099      public void postUpgrade(UpgradeContext context) throws ClientException
1100      {
1101        postponePostUpgrade(context);
1102      }
1103
1104      @Override
1105      public void postponePostUpgrade(UpgradeContext context) throws ClientException
1106      {
1107        if (reimportRequired)
1108        {
1109          context.notify(INFO_UPGRADE_TASK_RENAME_JE_DB_DIR_WARNING.get(), TextOutputCallback.WARNING);
1110        }
1111      }
1112
1113      @Override
1114      public String toString()
1115      {
1116        return INFO_UPGRADE_TASK_RENAME_JE_DB_DIR.get("%s", "%s").toString();
1117      }
1118    };
1119  }
1120
1121  /** This inner classes causes JE to be lazily linked and prevents runtime errors if JE is not in the classpath. */
1122  static final class JEHelper {
1123    private static ClientException clientException(final File backendDirectory, final DatabaseException e) {
1124      logger.error(LocalizableMessage.raw(StaticUtils.stackTraceToString(e)));
1125      return new ClientException(ReturnCode.CONSTRAINT_VIOLATION,
1126                                 INFO_UPGRADE_TASK_MIGRATE_JE_ENV_UNREADABLE.get(backendDirectory), e);
1127    }
1128
1129    static Set<String> listDatabases(final File backendDirectory) throws ClientException {
1130      try (Environment je = new Environment(backendDirectory, null)) {
1131        Set<String> databases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
1132        databases.addAll(je.getDatabaseNames());
1133        return databases;
1134      } catch (DatabaseException e) {
1135        throw clientException(backendDirectory, e);
1136      }
1137    }
1138
1139    static void migrateDatabases(final File envDir, final Map<String, String> renamedDbs) throws ClientException {
1140      EnvironmentConfig config = new EnvironmentConfig().setTransactional(true);
1141      try (Environment je = new Environment(envDir, config)) {
1142        final Transaction txn = je.beginTransaction(null, new TransactionConfig());
1143        try {
1144          for (String dbName : je.getDatabaseNames()) {
1145            String newDbName = renamedDbs.get(dbName);
1146            if (newDbName != null) {
1147              // id2entry or compressed schema should be kept
1148              je.renameDatabase(txn, dbName, newDbName);
1149            } else {
1150              // This index will need rebuilding
1151              je.removeDatabase(txn, dbName);
1152            }
1153          }
1154          txn.commit();
1155        } finally {
1156          txn.abort();
1157        }
1158      } catch (DatabaseException e) {
1159        throw JEHelper.clientException(envDir, e);
1160      }
1161    }
1162  }
1163
1164  private static void displayChangeCount(final String fileName,
1165      final int changeCount)
1166  {
1167    if (changeCount != 0)
1168    {
1169      logger.debug(INFO_UPGRADE_CHANGE_DONE_IN_SPECIFIC_FILE, fileName, changeCount);
1170    }
1171    else
1172    {
1173      logger.debug(INFO_UPGRADE_NO_CHANGE_DONE_IN_SPECIFIC_FILE, fileName);
1174    }
1175  }
1176
1177  private static void displayTaskLogInformation(final String summary,
1178      final String filter, final String... ldif)
1179  {
1180    logger.debug(LocalizableMessage.raw(summary));
1181    if (filter != null)
1182    {
1183      logger.debug(LocalizableMessage.raw(filter));
1184    }
1185    if (ldif != null)
1186    {
1187      logger.debug(LocalizableMessage.raw(Arrays.toString(ldif)));
1188    }
1189  }
1190
1191  private static void manageTaskException(final UpgradeContext context,
1192      final LocalizableMessage message, final ProgressNotificationCallback pnc)
1193      throws ClientException
1194  {
1195    countErrors++;
1196    context.notifyProgress(pnc.setProgress(-100));
1197    logger.error(message);
1198    if (!context.isIgnoreErrorsMode())
1199    {
1200      throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message);
1201    }
1202  }
1203
1204  private static UpgradeTask updateConfigEntry(final LocalizableMessage summary, final String filter,
1205      final ChangeOperationType changeOperationType, final String... ldif)
1206  {
1207    return new AbstractUpgradeTask()
1208    {
1209      @Override
1210      public void perform(final UpgradeContext context) throws ClientException
1211      {
1212        performConfigFileUpdate(summary, filter, changeOperationType, context, ldif);
1213      }
1214
1215      @Override
1216      public String toString()
1217      {
1218        return String.valueOf(summary);
1219      }
1220    };
1221  }
1222
1223  private static void performConfigFileUpdate(final LocalizableMessage summary, final String filter,
1224      final ChangeOperationType changeOperationType,
1225      final UpgradeContext context, final String... ldif)
1226      throws ClientException
1227  {
1228    displayTaskLogInformation(summary.toString(), filter, ldif);
1229
1230    final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20);
1231    context.notifyProgress(pnc);
1232
1233    try
1234    {
1235      final File configFile =
1236          new File(configDirectory, Installation.CURRENT_CONFIG_FILE_NAME);
1237
1238      final Filter filterVal = filter != null ? Filter.valueOf(filter) : null;
1239      final int changeCount = updateConfigFile(
1240          configFile.getPath(), filterVal, changeOperationType, ldif);
1241
1242      displayChangeCount(configFile.getPath(), changeCount);
1243
1244      context.notifyProgress(pnc.setProgress(100));
1245    }
1246    catch (final Exception e)
1247    {
1248      manageTaskException(context, LocalizableMessage.raw(e.getMessage()), pnc);
1249    }
1250  }
1251
1252  static UpgradeTask clearReplicationDbDirectory()
1253  {
1254    return new AbstractUpgradeTask()
1255    {
1256      private File replicationDbDir;
1257
1258      @Override
1259      public void prepare(final UpgradeContext context) throws ClientException
1260      {
1261        String replDbDir = readReplicationDbDirFromConfig();
1262        if (replDbDir != null
1263            && context.confirmYN(INFO_UPGRADE_TASK_MIGRATE_CHANGELOG_DESCRIPTION.get(), NO) == YES)
1264        {
1265          replicationDbDir = new File(getInstancePath(), replDbDir).getAbsoluteFile();
1266        }
1267        // if replDbDir == null, then this is not an RS, there is no changelog DB to clear
1268      }
1269
1270      private String readReplicationDbDirFromConfig() throws ClientException
1271      {
1272        final SearchRequest sr = Requests.newSearchRequest(
1273            DN.valueOf("cn=replication server,cn=Multimaster Synchronization,cn=Synchronization Providers,cn=config"),
1274            SearchScope.BASE_OBJECT, Filter.alwaysTrue());
1275        try (final EntryReader entryReader = searchConfigFile(sr))
1276        {
1277          if (entryReader.hasNext())
1278          {
1279            final Entry replServerCfg = entryReader.readEntry();
1280            return replServerCfg.parseAttribute("ds-cfg-replication-db-directory").asString();
1281          }
1282          return null;
1283        }
1284        catch (IOException e)
1285        {
1286          LocalizableMessage msg = INFO_UPGRADE_TASK_MIGRATE_CONFIG_READ_FAIL.get();
1287          throw new ClientException(ReturnCode.APPLICATION_ERROR, msg, e);
1288        }
1289      }
1290
1291      @Override
1292      public void perform(final UpgradeContext context) throws ClientException
1293      {
1294        if (replicationDbDir == null)
1295        {
1296          // there is no changelog DB to clear
1297          return;
1298        }
1299
1300        LocalizableMessage msg = INFO_UPGRADE_TASK_DELETE_CHANGELOG_SUMMARY.get(replicationDbDir);
1301        ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
1302        context.notifyProgress(pnc);
1303        try
1304        {
1305          FileManager.deleteRecursively(replicationDbDir);
1306          context.notifyProgress(pnc.setProgress(100));
1307        }
1308        catch (ClientException e)
1309        {
1310          manageTaskException(context, e.getMessageObject(), pnc);
1311        }
1312        catch (Exception e)
1313        {
1314          manageTaskException(context, LocalizableMessage.raw(e.getLocalizedMessage()), pnc);
1315        }
1316      }
1317
1318      @Override
1319      public String toString()
1320      {
1321        return INFO_UPGRADE_TASK_DELETE_CHANGELOG_SUMMARY.get(replicationDbDir).toString();
1322      }
1323    };
1324  }
1325
1326  /** Prevent instantiation. */
1327  private UpgradeTasks()
1328  {
1329    // Do nothing.
1330  }
1331}