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