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