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