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