001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS.
026 */
027package org.opends.server.tools;
028
029import static org.opends.messages.ToolMessages.*;
030import static org.opends.server.protocols.ldap.LDAPResultCode.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import static com.forgerock.opendj.cli.ArgumentConstants.*;
034import static com.forgerock.opendj.cli.Utils.*;
035
036import java.io.File;
037import java.io.IOException;
038import java.io.OutputStream;
039import java.io.PrintStream;
040import java.util.HashMap;
041import java.util.LinkedHashMap;
042import java.util.LinkedList;
043import java.util.List;
044import java.util.Map;
045import java.util.TreeMap;
046
047import org.forgerock.i18n.LocalizableMessage;
048import org.forgerock.opendj.ldap.ByteString;
049import org.opends.server.core.DirectoryServer;
050import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
051import org.opends.server.extensions.ConfigFileHandler;
052import org.opends.server.loggers.JDKLogging;
053import org.opends.server.types.Attribute;
054import org.opends.server.types.AttributeType;
055import org.opends.server.types.DN;
056import org.opends.server.types.DirectoryException;
057import org.opends.server.types.Entry;
058import org.opends.server.types.ExistingFileBehavior;
059import org.opends.server.types.InitializationException;
060import org.opends.server.types.LDAPException;
061import org.opends.server.types.LDIFExportConfig;
062import org.opends.server.types.LDIFImportConfig;
063import org.opends.server.types.Modification;
064import org.opends.server.types.NullOutputStream;
065import org.opends.server.types.ObjectClass;
066import org.opends.server.types.RawModification;
067import org.opends.server.util.AddChangeRecordEntry;
068import org.opends.server.util.BuildVersion;
069import org.opends.server.util.ChangeRecordEntry;
070import org.opends.server.util.DeleteChangeRecordEntry;
071import org.opends.server.util.LDIFException;
072import org.opends.server.util.LDIFReader;
073import org.opends.server.util.LDIFWriter;
074import org.opends.server.util.ModifyChangeRecordEntry;
075
076import com.forgerock.opendj.cli.ArgumentException;
077import com.forgerock.opendj.cli.ArgumentParser;
078import com.forgerock.opendj.cli.BooleanArgument;
079import com.forgerock.opendj.cli.CommonArguments;
080import com.forgerock.opendj.cli.StringArgument;
081
082/**
083 * This class provides a program that may be used to apply a set of changes (in
084 * LDIF change format) to an LDIF file.  It will first read all of the changes
085 * into memory, and then will iterate through an LDIF file and apply them to the
086 * entries contained in it.  Note that because of the manner in which it
087 * processes the changes, certain types of operations will not be allowed,
088 * including:
089 * <BR>
090 * <UL>
091 *   <LI>Modify DN operations</LI>
092 *   <LI>Deleting an entry that has been added</LI>
093 *   <LI>Modifying an entry that has been added</LI>
094 * </UL>
095 */
096public class LDIFModify
097{
098  /**
099   * The fully-qualified name of this class.
100   */
101  private static final String CLASS_NAME = "org.opends.server.tools.LDIFModify";
102
103
104
105  /**
106   * Applies the specified changes to the source LDIF, writing the modified
107   * file to the specified target.  Neither the readers nor the writer will be
108   * closed.
109   *
110   * @param  sourceReader  The LDIF reader that will be used to read the LDIF
111   *                       content to be modified.
112   * @param  changeReader  The LDIF reader that will be used to read the changes
113   *                       to be applied.
114   * @param  targetWriter  The LDIF writer that will be used to write the
115   *                       modified LDIF.
116   * @param  errorList     A list into which any error messages generated while
117   *                       processing changes may be added.
118   *
119   * @return  <CODE>true</CODE> if all updates were successfully applied, or
120   *          <CODE>false</CODE> if any errors were encountered.
121   *
122   * @throws  IOException  If a problem occurs while attempting to read the
123   *                       source or changes, or write the target.
124   *
125   * @throws  LDIFException  If a problem occurs while attempting to decode the
126   *                         source or changes, or trying to determine whether
127   *                         to include the entry in the output.
128   */
129  public static boolean modifyLDIF(LDIFReader sourceReader,
130                                   LDIFReader changeReader,
131                                   LDIFWriter targetWriter,
132                                   List<LocalizableMessage> errorList)
133         throws IOException, LDIFException
134  {
135    // Read the changes into memory.
136    TreeMap<DN,AddChangeRecordEntry> adds = new TreeMap<>();
137    TreeMap<DN,Entry> ldifEntries = new TreeMap<>();
138    HashMap<DN,DeleteChangeRecordEntry> deletes = new HashMap<>();
139    HashMap<DN,LinkedList<Modification>> modifications = new HashMap<>();
140
141    while (true)
142    {
143      ChangeRecordEntry changeRecord;
144      try
145      {
146        changeRecord = changeReader.readChangeRecord(false);
147      }
148      catch (LDIFException le)
149      {
150        if (le.canContinueReading())
151        {
152          errorList.add(le.getMessageObject());
153          continue;
154        }
155        else
156        {
157          throw le;
158        }
159      }
160
161      if (changeRecord == null)
162      {
163        break;
164      }
165
166      DN changeDN = changeRecord.getDN();
167      switch (changeRecord.getChangeOperationType())
168      {
169        case ADD:
170          // The entry must not exist in the add list.
171          if (adds.containsKey(changeDN))
172          {
173            errorList.add(ERR_LDIFMODIFY_CANNOT_ADD_ENTRY_TWICE.get(changeDN));
174            continue;
175          }
176          else
177          {
178            adds.put(changeDN, (AddChangeRecordEntry) changeRecord);
179          }
180          break;
181
182        case DELETE:
183          // The entry must not exist in the add list.  If it exists in the
184          // modify list, then remove the changes since we won't need to apply
185          // them.
186          if (adds.containsKey(changeDN))
187          {
188            errorList.add(ERR_LDIFMODIFY_CANNOT_DELETE_AFTER_ADD.get(changeDN));
189            continue;
190          }
191          else
192          {
193            modifications.remove(changeDN);
194            deletes.put(changeDN, (DeleteChangeRecordEntry) changeRecord);
195          }
196          break;
197
198        case MODIFY:
199          // The entry must not exist in the add or delete lists.
200          if (adds.containsKey(changeDN) || deletes.containsKey(changeDN))
201          {
202            errorList.add(ERR_LDIFMODIFY_CANNOT_MODIFY_ADDED_OR_DELETED.get(changeDN));
203            continue;
204          }
205          else
206          {
207            LinkedList<Modification> mods =
208                 modifications.get(changeDN);
209            if (mods == null)
210            {
211              mods = new LinkedList<>();
212              modifications.put(changeDN, mods);
213            }
214
215            for (RawModification mod :
216                 ((ModifyChangeRecordEntry) changeRecord).getModifications())
217            {
218              try
219              {
220                mods.add(mod.toModification());
221              }
222              catch (LDAPException le)
223              {
224                errorList.add(le.getMessageObject());
225                continue;
226              }
227            }
228          }
229          break;
230
231        case MODIFY_DN:
232          errorList.add(ERR_LDIFMODIFY_MODDN_NOT_SUPPORTED.get(changeDN));
233          continue;
234
235        default:
236          errorList.add(ERR_LDIFMODIFY_UNKNOWN_CHANGETYPE.get(changeDN, changeRecord.getChangeOperationType()));
237          continue;
238      }
239    }
240
241
242    // Read the source an entry at a time and apply any appropriate changes
243    // before writing to the target LDIF.
244    while (true)
245    {
246      Entry entry;
247      try
248      {
249        entry = sourceReader.readEntry();
250      }
251      catch (LDIFException le)
252      {
253        if (le.canContinueReading())
254        {
255          errorList.add(le.getMessageObject());
256          continue;
257        }
258        else
259        {
260          throw le;
261        }
262      }
263
264      if (entry == null)
265      {
266        break;
267      }
268
269
270      // If the entry is to be deleted, then just skip over it without writing
271      // it to the output.
272      DN entryDN = entry.getName();
273      if (deletes.remove(entryDN) != null)
274      {
275        continue;
276      }
277
278
279      // If the entry is to be added, then that's an error, since it already
280      // exists.
281      if (adds.remove(entryDN) != null)
282      {
283        errorList.add(ERR_LDIFMODIFY_ADD_ALREADY_EXISTS.get(entryDN));
284        continue;
285      }
286
287
288      // If the entry is to be modified, then process the changes.
289      LinkedList<Modification> mods = modifications.remove(entryDN);
290      if (mods != null && !mods.isEmpty())
291      {
292        try
293        {
294          entry.applyModifications(mods);
295        }
296        catch (DirectoryException de)
297        {
298          errorList.add(de.getMessageObject());
299          continue;
300        }
301      }
302
303
304      // If we've gotten here, then the (possibly updated) entry should be
305      // written to the LDIF entry Map.
306      ldifEntries.put(entry.getName(),entry);
307    }
308
309
310    // Perform any adds that may be necessary.
311    for (AddChangeRecordEntry add : adds.values())
312    {
313      Map<ObjectClass,String> objectClasses = new LinkedHashMap<>();
314      Map<AttributeType,List<Attribute>> userAttributes = new LinkedHashMap<>();
315      Map<AttributeType,List<Attribute>> operationalAttributes = new LinkedHashMap<>();
316
317      for (Attribute a : add.getAttributes())
318      {
319        AttributeType t = a.getAttributeType();
320        if (t.isObjectClass())
321        {
322          for (ByteString v : a)
323          {
324            String stringValue = v.toString();
325            String lowerValue  = toLowerCase(stringValue);
326            ObjectClass oc = DirectoryServer.getObjectClass(lowerValue, true);
327            objectClasses.put(oc, stringValue);
328          }
329        }
330        else if (t.isOperational())
331        {
332          List<Attribute> attrList = operationalAttributes.get(t);
333          if (attrList == null)
334          {
335            attrList = new LinkedList<>();
336            operationalAttributes.put(t, attrList);
337          }
338          attrList.add(a);
339        }
340        else
341        {
342          List<Attribute> attrList = userAttributes.get(t);
343          if (attrList == null)
344          {
345            attrList = new LinkedList<>();
346            userAttributes.put(t, attrList);
347          }
348          attrList.add(a);
349        }
350      }
351
352      Entry e = new Entry(add.getDN(), objectClasses, userAttributes,
353                          operationalAttributes);
354      //Put the entry to be added into the LDIF entry map.
355      ldifEntries.put(e.getName(),e);
356    }
357
358
359    // If there are any entries left in the delete or modify lists, then that's
360    // a problem because they didn't exist.
361    if (! deletes.isEmpty())
362    {
363      for (DN dn : deletes.keySet())
364      {
365        errorList.add(ERR_LDIFMODIFY_DELETE_NO_SUCH_ENTRY.get(dn));
366      }
367    }
368
369    if (! modifications.isEmpty())
370    {
371      for (DN dn : modifications.keySet())
372      {
373        errorList.add(ERR_LDIFMODIFY_MODIFY_NO_SUCH_ENTRY.get(dn));
374      }
375    }
376    return targetWriter.writeEntries(ldifEntries.values()) &&
377            errorList.isEmpty();
378  }
379
380
381
382  /**
383   * Invokes <CODE>ldifModifyMain</CODE> to perform the appropriate processing.
384   *
385   * @param  args  The command-line arguments provided to the client.
386   */
387  public static void main(String[] args)
388  {
389    int returnCode = ldifModifyMain(args, false, System.out, System.err);
390    if (returnCode != 0)
391    {
392      System.exit(filterExitCode(returnCode));
393    }
394  }
395
396
397
398  /**
399   * Processes the command-line arguments and makes the appropriate updates to
400   * the LDIF file.
401   *
402   * @param  args               The command line arguments provided to this
403   *                            program.
404   * @param  serverInitialized  Indicates whether the Directory Server has
405   *                            already been initialized (and therefore should
406   *                            not be initialized a second time).
407   * @param  outStream          The output stream to use for standard output, or
408   *                            {@code null} if standard output is not needed.
409   * @param  errStream          The output stream to use for standard error, or
410   *                            {@code null} if standard error is not needed.
411   *
412   * @return  A value of zero if everything completed properly, or nonzero if
413   *          any problem(s) occurred.
414   */
415  public static int ldifModifyMain(String[] args, boolean serverInitialized,
416                                   OutputStream outStream,
417                                   OutputStream errStream)
418  {
419    PrintStream err = NullOutputStream.wrapOrNullStream(errStream);
420    JDKLogging.disableLogging();
421
422    // Prepare the argument parser.
423    BooleanArgument showUsage;
424    StringArgument  changesFile;
425    StringArgument  configClass;
426    StringArgument  configFile;
427    StringArgument  sourceFile;
428    StringArgument  targetFile;
429
430    LocalizableMessage toolDescription = INFO_LDIFMODIFY_TOOL_DESCRIPTION.get();
431    ArgumentParser argParser = new ArgumentParser(CLASS_NAME, toolDescription,
432                                                  false);
433    argParser.setShortToolDescription(REF_SHORT_DESC_LDIFMODIFY.get());
434    argParser.setVersionHandler(new DirectoryServerVersionHandler());
435
436    try
437    {
438      configFile = new StringArgument("configfile", 'c', "configFile", true,
439                                      false, true,
440                                      INFO_CONFIGFILE_PLACEHOLDER.get(), null,
441                                      null,
442                                      INFO_DESCRIPTION_CONFIG_FILE.get());
443      configFile.setHidden(true);
444      argParser.addArgument(configFile);
445
446
447      configClass = new StringArgument("configclass", OPTION_SHORT_CONFIG_CLASS,
448                             OPTION_LONG_CONFIG_CLASS, false,
449                             false, true, INFO_CONFIGCLASS_PLACEHOLDER.get(),
450                             ConfigFileHandler.class.getName(), null,
451                             INFO_DESCRIPTION_CONFIG_CLASS.get());
452      configClass.setHidden(true);
453      argParser.addArgument(configClass);
454
455
456      sourceFile = new StringArgument("sourceldif", 's', "sourceLDIF", true,
457                                      false, true,
458                                      INFO_LDIFFILE_PLACEHOLDER.get(), null,
459                                      null,
460                                      INFO_LDIFMODIFY_DESCRIPTION_SOURCE.get());
461      argParser.addArgument(sourceFile);
462
463
464      changesFile =
465              new StringArgument("changesldif", 'm', "changesLDIF", true,
466                                 false, true, INFO_LDIFFILE_PLACEHOLDER.get(),
467                                 null, null,
468                                 INFO_LDIFMODIFY_DESCRIPTION_CHANGES.get());
469      argParser.addArgument(changesFile);
470
471
472      targetFile = new StringArgument("targetldif", 't', "targetLDIF", true,
473                                      false, true,
474                                      INFO_LDIFFILE_PLACEHOLDER.get(), null,
475                                      null,
476                                      INFO_LDIFMODIFY_DESCRIPTION_TARGET.get());
477      argParser.addArgument(targetFile);
478
479
480      showUsage = CommonArguments.getShowUsage();
481      argParser.addArgument(showUsage);
482      argParser.setUsageArgument(showUsage);
483    }
484    catch (ArgumentException ae)
485    {
486      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
487      return 1;
488    }
489
490
491    // Parse the command-line arguments provided to the program.
492    try
493    {
494      argParser.parseArguments(args);
495    }
496    catch (ArgumentException ae)
497    {
498      argParser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
499      return CLIENT_SIDE_PARAM_ERROR;
500    }
501
502
503    // If we should just display usage or version information,
504    // then print it and exit.
505    if (argParser.usageOrVersionDisplayed())
506    {
507      return 0;
508    }
509
510    // Checks the version - if upgrade required, the tool is unusable
511    try
512    {
513      BuildVersion.checkVersionMismatch();
514    }
515    catch (InitializationException e)
516    {
517      printWrappedText(err, e.getMessage());
518      return 1;
519    }
520
521    if (! serverInitialized)
522    {
523      // Bootstrap the Directory Server configuration for use as a client.
524      DirectoryServer directoryServer = DirectoryServer.getInstance();
525      DirectoryServer.bootstrapClient();
526
527
528      // If we're to use the configuration then initialize it, along with the
529      // schema.
530      boolean checkSchema = configFile.isPresent();
531      if (checkSchema)
532      {
533        try
534        {
535          DirectoryServer.initializeJMX();
536        }
537        catch (Exception e)
538        {
539          printWrappedText(err, ERR_LDIFMODIFY_CANNOT_INITIALIZE_JMX.get(configFile.getValue(), e.getMessage()));
540          return 1;
541        }
542
543        try
544        {
545          directoryServer.initializeConfiguration(configClass.getValue(),
546                                                  configFile.getValue());
547        }
548        catch (Exception e)
549        {
550          printWrappedText(err, ERR_LDIFMODIFY_CANNOT_INITIALIZE_CONFIG.get(configFile.getValue(), e.getMessage()));
551          return 1;
552        }
553
554        try
555        {
556          directoryServer.initializeSchema();
557        }
558        catch (Exception e)
559        {
560          printWrappedText(err, ERR_LDIFMODIFY_CANNOT_INITIALIZE_SCHEMA.get(configFile.getValue(), e.getMessage()));
561          return 1;
562        }
563      }
564    }
565
566
567    // Create the LDIF readers and writer from the arguments.
568    File source = new File(sourceFile.getValue());
569    if (! source.exists())
570    {
571      printWrappedText(err, ERR_LDIFMODIFY_SOURCE_DOES_NOT_EXIST.get(sourceFile.getValue()));
572      return CLIENT_SIDE_PARAM_ERROR;
573    }
574
575    LDIFImportConfig importConfig = new LDIFImportConfig(sourceFile.getValue());
576    LDIFReader sourceReader;
577    try
578    {
579      sourceReader = new LDIFReader(importConfig);
580    }
581    catch (IOException ioe)
582    {
583      printWrappedText(err, ERR_LDIFMODIFY_CANNOT_OPEN_SOURCE.get(sourceFile.getValue(), ioe));
584      return CLIENT_SIDE_LOCAL_ERROR;
585    }
586
587
588    File changes = new File(changesFile.getValue());
589    if (! changes.exists())
590    {
591      printWrappedText(err, ERR_LDIFMODIFY_CHANGES_DOES_NOT_EXIST.get(changesFile.getValue()));
592      return CLIENT_SIDE_PARAM_ERROR;
593    }
594
595    importConfig = new LDIFImportConfig(changesFile.getValue());
596    LDIFReader changeReader;
597    try
598    {
599      changeReader = new LDIFReader(importConfig);
600    }
601    catch (IOException ioe)
602    {
603      printWrappedText(err, ERR_LDIFMODIFY_CANNOT_OPEN_CHANGES.get(sourceFile.getValue(), ioe.getMessage()));
604      return CLIENT_SIDE_LOCAL_ERROR;
605    }
606
607
608    LDIFExportConfig exportConfig =
609         new LDIFExportConfig(targetFile.getValue(),
610                              ExistingFileBehavior.OVERWRITE);
611    LDIFWriter targetWriter;
612    try
613    {
614      targetWriter = new LDIFWriter(exportConfig);
615    }
616    catch (IOException ioe)
617    {
618      printWrappedText(err, ERR_LDIFMODIFY_CANNOT_OPEN_TARGET.get(sourceFile.getValue(), ioe.getMessage()));
619      return CLIENT_SIDE_LOCAL_ERROR;
620    }
621
622
623    // Actually invoke the LDIF processing.
624    LinkedList<LocalizableMessage> errorList = new LinkedList<>();
625    boolean successful;
626    try
627    {
628      successful = modifyLDIF(sourceReader, changeReader, targetWriter, errorList);
629    }
630    catch (Exception e)
631    {
632      err.println(ERR_LDIFMODIFY_ERROR_PROCESSING_LDIF.get(e));
633      successful = false;
634    }
635
636    close(sourceReader, changeReader, targetWriter);
637
638    for (LocalizableMessage s : errorList)
639    {
640      err.println(s);
641    }
642    return successful ? 0 : 1;
643  }
644}