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.util;
028    import org.opends.messages.Message;
029    import org.opends.messages.MessageBuilder;
030    
031    
032    import static org.opends.server.loggers.debug.DebugLogger.*;
033    import org.opends.server.loggers.debug.DebugTracer;
034    import static org.opends.server.loggers.ErrorLogger.logError;
035    import static org.opends.messages.UtilityMessages.*;
036    import static org.opends.server.util.StaticUtils.toLowerCase;
037    import static org.opends.server.util.Validator.*;
038    
039    import java.io.BufferedReader;
040    import java.io.BufferedWriter;
041    import java.io.ByteArrayOutputStream;
042    import java.io.IOException;
043    import java.io.InputStream;
044    import java.net.URL;
045    import java.util.ArrayList;
046    import java.util.HashMap;
047    import java.util.LinkedHashSet;
048    import java.util.LinkedList;
049    import java.util.List;
050    
051    import org.opends.server.core.DirectoryServer;
052    import org.opends.server.core.PluginConfigManager;
053    import org.opends.server.protocols.asn1.ASN1OctetString;
054    import org.opends.server.protocols.ldap.LDAPAttribute;
055    import org.opends.server.protocols.ldap.LDAPModification;
056    import org.opends.server.types.AcceptRejectWarn;
057    import org.opends.server.types.Attribute;
058    import org.opends.server.types.AttributeType;
059    import org.opends.server.types.AttributeValue;
060    import org.opends.server.types.DirectoryException;
061    import org.opends.server.types.DN;
062    import org.opends.server.types.DebugLogLevel;
063    import org.opends.server.types.Entry;
064    
065    
066    import org.opends.server.types.LDIFImportConfig;
067    import org.opends.server.types.ModificationType;
068    import org.opends.server.types.ObjectClass;
069    import org.opends.server.types.RawModification;
070    import org.opends.server.types.RDN;
071    import org.opends.server.api.plugin.PluginResult;
072    
073    
074    /**
075     * This class provides the ability to read information from an LDIF file.  It
076     * provides support for both standard entries and change entries (as would be
077     * used with a tool like ldapmodify).
078     */
079    @org.opends.server.types.PublicAPI(
080         stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
081         mayInstantiate=true,
082         mayExtend=false,
083         mayInvoke=true)
084    public final class LDIFReader
085    {
086      /**
087       * The tracer object for the debug logger.
088       */
089      private static final DebugTracer TRACER = getTracer();
090    
091      // The reader that will be used to read the data.
092      private BufferedReader reader;
093    
094      // The buffer to use to read data from a URL.
095      private byte[] buffer;
096    
097      // The import configuration that specifies what should be imported.
098      private LDIFImportConfig importConfig;
099    
100      // The lines that comprise the body of the last entry read.
101      private LinkedList<StringBuilder> lastEntryBodyLines;
102    
103      // The lines that comprise the header (DN and any comments) for the last entry
104      // read.
105      private LinkedList<StringBuilder> lastEntryHeaderLines;
106    
107      // The number of entries that have been ignored by this LDIF reader because
108      // they didn't match the criteria.
109      private long entriesIgnored;
110    
111      // The number of entries that have been read by this LDIF reader, including
112      // those that were ignored because they didn't match the criteria, and
113      // including those that were rejected because they were invalid in some way.
114      private long entriesRead;
115    
116      // The number of entries that have been rejected by this LDIF reader.
117      private long entriesRejected;
118    
119      // The line number on which the last entry started.
120      private long lastEntryLineNumber;
121    
122      // The line number of the last line read from the LDIF file, starting with 1.
123      private long lineNumber;
124    
125      // The plugin config manager that will be used if we are to invoke plugins
126      // on the entries as they are read.
127      private PluginConfigManager pluginConfigManager;
128    
129    
130    
131      /**
132       * Creates a new LDIF reader that will read information from the specified
133       * file.
134       *
135       * @param  importConfig  The import configuration for this LDIF reader.  It
136       *                       must not be <CODE>null</CODE>.
137       *
138       * @throws  IOException  If a problem occurs while opening the LDIF file for
139       *                       reading.
140       */
141      public LDIFReader(LDIFImportConfig importConfig)
142             throws IOException
143      {
144        ensureNotNull(importConfig);
145        this.importConfig = importConfig;
146    
147        reader               = importConfig.getReader();
148        buffer               = new byte[4096];
149        entriesRead          = 0;
150        entriesIgnored       = 0;
151        entriesRejected      = 0;
152        lineNumber           = 0;
153        lastEntryLineNumber  = -1;
154        lastEntryBodyLines   = new LinkedList<StringBuilder>();
155        lastEntryHeaderLines = new LinkedList<StringBuilder>();
156        pluginConfigManager  = DirectoryServer.getPluginConfigManager();
157      }
158    
159    
160    
161      /**
162       * Reads the next entry from the LDIF source.
163       *
164       * @return  The next entry read from the LDIF source, or <CODE>null</CODE> if
165       *          the end of the LDIF data is reached.
166       *
167       * @throws  IOException  If an I/O problem occurs while reading from the file.
168       *
169       * @throws  LDIFException  If the information read cannot be parsed as an LDIF
170       *                         entry.
171       */
172      public Entry readEntry()
173             throws IOException, LDIFException
174      {
175        return readEntry(importConfig.validateSchema());
176      }
177    
178    
179    
180      /**
181       * Reads the next entry from the LDIF source.
182       *
183       * @param  checkSchema  Indicates whether this reader should perform schema
184       *                      checking on the entry before returning it to the
185       *                      caller.  Note that some basic schema checking (like
186       *                      refusing multiple values for a single-valued
187       *                      attribute) may always be performed.
188       *
189       *
190       * @return  The next entry read from the LDIF source, or <CODE>null</CODE> if
191       *          the end of the LDIF data is reached.
192       *
193       * @throws  IOException  If an I/O problem occurs while reading from the file.
194       *
195       * @throws  LDIFException  If the information read cannot be parsed as an LDIF
196       *                         entry.
197       */
198      public Entry readEntry(boolean checkSchema)
199             throws IOException, LDIFException
200      {
201        while (true)
202        {
203          // Read the set of lines that make up the next entry.
204          LinkedList<StringBuilder> lines = readEntryLines();
205          if (lines == null)
206          {
207            return null;
208          }
209          lastEntryBodyLines   = lines;
210          lastEntryHeaderLines = new LinkedList<StringBuilder>();
211    
212    
213          // Read the DN of the entry and see if it is one that should be included
214          // in the import.
215          DN entryDN = readDN(lines);
216          if (entryDN == null)
217          {
218            // This should only happen if the LDIF starts with the "version:" line
219            // and has a blank line immediately after that.  In that case, simply
220            // read and return the next entry.
221            continue;
222          }
223          else if (!importConfig.includeEntry(entryDN))
224          {
225            if (debugEnabled())
226            {
227              TRACER.debugInfo("Skipping entry %s because the DN is not one that " +
228                  "should be included based on the include and exclude branches.",
229                        entryDN);
230            }
231            entriesRead++;
232            Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
233            logToSkipWriter(lines, message);
234            entriesIgnored++;
235            continue;
236          }
237          else
238          {
239            entriesRead++;
240          }
241    
242          // Read the set of attributes from the entry.
243          HashMap<ObjectClass,String> objectClasses =
244               new HashMap<ObjectClass,String>();
245          HashMap<AttributeType,List<Attribute>> userAttributes =
246               new HashMap<AttributeType,List<Attribute>>();
247          HashMap<AttributeType,List<Attribute>> operationalAttributes =
248               new HashMap<AttributeType,List<Attribute>>();
249          try
250          {
251            for (StringBuilder line : lines)
252            {
253              readAttribute(lines, line, entryDN, objectClasses, userAttributes,
254                            operationalAttributes, checkSchema);
255            }
256          }
257          catch (LDIFException e)
258          {
259            entriesRejected++;
260            throw e;
261          }
262    
263          // Create the entry and see if it is one that should be included in the
264          // import.
265          Entry entry =  new Entry(entryDN, objectClasses, userAttributes,
266                                   operationalAttributes);
267          TRACER.debugProtocolElement(DebugLogLevel.VERBOSE, entry);
268    
269          try
270          {
271            if (! importConfig.includeEntry(entry))
272            {
273              if (debugEnabled())
274              {
275                TRACER.debugInfo("Skipping entry %s because the DN is not one " +
276                    "that should be included based on the include and exclude " +
277                    "filters.", entryDN);
278              }
279              Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN));
280              logToSkipWriter(lines, message);
281              entriesIgnored++;
282              continue;
283            }
284          }
285          catch (Exception e)
286          {
287            if (debugEnabled())
288            {
289              TRACER.debugCaught(DebugLogLevel.ERROR, e);
290            }
291    
292            Message message = ERR_LDIF_COULD_NOT_EVALUATE_FILTERS_FOR_IMPORT.
293                get(String.valueOf(entry.getDN()), lastEntryLineNumber,
294                    String.valueOf(e));
295            throw new LDIFException(message, lastEntryLineNumber, true, e);
296          }
297    
298    
299          // If we should invoke import plugins, then do so.
300          if (importConfig.invokeImportPlugins())
301          {
302            PluginResult.ImportLDIF pluginResult =
303                 pluginConfigManager.invokeLDIFImportPlugins(importConfig, entry);
304            if (! pluginResult.continueProcessing())
305            {
306              Message m;
307              Message rejectMessage = pluginResult.getErrorMessage();
308              if (rejectMessage == null)
309              {
310                m = ERR_LDIF_REJECTED_BY_PLUGIN_NOMESSAGE.get(
311                         String.valueOf(entryDN));
312              }
313              else
314              {
315                m = ERR_LDIF_REJECTED_BY_PLUGIN.get(String.valueOf(entryDN),
316                                                    rejectMessage);
317              }
318    
319              logToRejectWriter(lines, m);
320              entriesRejected++;
321              continue;
322            }
323          }
324    
325    
326          // Make sure that the entry is valid as per the server schema if it is
327          // appropriate to do so.
328          if (checkSchema)
329          {
330            MessageBuilder invalidReason = new MessageBuilder();
331            if (! entry.conformsToSchema(null, false, true, false, invalidReason))
332            {
333              Message message = ERR_LDIF_SCHEMA_VIOLATION.get(
334                      String.valueOf(entryDN),
335                      lastEntryLineNumber,
336                      invalidReason.toString());
337              logToRejectWriter(lines, message);
338              entriesRejected++;
339              throw new LDIFException(message, lastEntryLineNumber, true);
340            }
341          }
342    
343    
344          // The entry should be included in the import, so return it.
345          return entry;
346        }
347      }
348    
349      /**
350       * Reads the next change record from the LDIF source.
351       *
352       * @param  defaultAdd  Indicates whether the change type should default to
353       *                     "add" if none is explicitly provided.
354       *
355       * @return  The next change record from the LDIF source, or <CODE>null</CODE>
356       *          if the end of the LDIF data is reached.
357       *
358       * @throws  IOException  If an I/O problem occurs while reading from the file.
359       *
360       * @throws  LDIFException  If the information read cannot be parsed as an LDIF
361       *                         entry.
362       */
363      public ChangeRecordEntry readChangeRecord(boolean defaultAdd)
364             throws IOException, LDIFException
365      {
366        while (true)
367        {
368          // Read the set of lines that make up the next entry.
369          LinkedList<StringBuilder> lines = readEntryLines();
370          if (lines == null)
371          {
372            return null;
373          }
374    
375    
376          // Read the DN of the entry and see if it is one that should be included
377          // in the import.
378          DN entryDN = readDN(lines);
379          if (entryDN == null)
380          {
381            // This should only happen if the LDIF starts with the "version:" line
382            // and has a blank line immediately after that.  In that case, simply
383            // read and return the next entry.
384            continue;
385          }
386    
387          String changeType = readChangeType(lines);
388    
389          ChangeRecordEntry entry = null;
390    
391          if(changeType != null)
392          {
393            if(changeType.equals("add"))
394            {
395              entry = parseAddChangeRecordEntry(entryDN, lines);
396            } else if (changeType.equals("delete"))
397            {
398              entry = parseDeleteChangeRecordEntry(entryDN, lines);
399            } else if (changeType.equals("modify"))
400            {
401              entry = parseModifyChangeRecordEntry(entryDN, lines);
402            } else if (changeType.equals("modrdn"))
403            {
404              entry = parseModifyDNChangeRecordEntry(entryDN, lines);
405            } else if (changeType.equals("moddn"))
406            {
407              entry = parseModifyDNChangeRecordEntry(entryDN, lines);
408            } else
409            {
410              Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get(
411                  changeType, "add, delete, modify, moddn, modrdn");
412              throw new LDIFException(message, lastEntryLineNumber, false);
413            }
414          } else
415          {
416            // default to "add"?
417            if(defaultAdd)
418            {
419              entry = parseAddChangeRecordEntry(entryDN, lines);
420            } else
421            {
422              Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get(
423                  null, "add, delete, modify, moddn, modrdn");
424              throw new LDIFException(message, lastEntryLineNumber, false);
425            }
426          }
427    
428          return entry;
429        }
430      }
431    
432    
433    
434      /**
435       * Reads a set of lines from the next entry in the LDIF source.
436       *
437       * @return  A set of lines from the next entry in the LDIF source.
438       *
439       * @throws  IOException  If a problem occurs while reading from the LDIF
440       *                       source.
441       *
442       * @throws  LDIFException  If the information read is not valid LDIF.
443       */
444      private LinkedList<StringBuilder> readEntryLines()
445              throws IOException, LDIFException
446      {
447        // Read the entry lines into a buffer.
448        LinkedList<StringBuilder> lines = new LinkedList<StringBuilder>();
449        int lastLine = -1;
450    
451        while (true)
452        {
453          String line = reader.readLine();
454          lineNumber++;
455    
456          if (line == null)
457          {
458            // This must mean that we have reached the end of the LDIF source.
459            // If the set of lines read so far is empty, then move onto the next
460            // file or return null.  Otherwise, break out of this loop.
461            if (lines.isEmpty())
462            {
463              reader = importConfig.nextReader();
464              if (reader == null)
465              {
466                return null;
467              }
468              else
469              {
470                return readEntryLines();
471              }
472            }
473            else
474            {
475              break;
476            }
477          }
478          else if (line.length() == 0)
479          {
480            // This is a blank line.  If the set of lines read so far is empty,
481            // then just skip over it.  Otherwise, break out of this loop.
482            if (lines.isEmpty())
483            {
484              continue;
485            }
486            else
487            {
488              break;
489            }
490          }
491          else if (line.charAt(0) == '#')
492          {
493            // This is a comment.  Ignore it.
494            continue;
495          }
496          else if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t'))
497          {
498            // This is a continuation of the previous line.  If there is no
499            // previous line, then that's a problem.  Note that while RFC 2849
500            // technically only allows a space in this position, both OpenLDAP and
501            // the Sun Java System Directory Server allow a tab as well, so we will
502            // too for compatibility reasons.  See issue #852 for details.
503            if (lastLine >= 0)
504            {
505              lines.get(lastLine).append(line.substring(1));
506            }
507            else
508            {
509              Message message =
510                      ERR_LDIF_INVALID_LEADING_SPACE.get(lineNumber, line);
511              logToRejectWriter(lines, message);
512              throw new LDIFException(message, lineNumber, false);
513            }
514          }
515          else
516          {
517            // This is a new line.
518            if (lines.isEmpty())
519            {
520              lastEntryLineNumber = lineNumber;
521            }
522            lines.add(new StringBuilder(line));
523            lastLine++;
524          }
525        }
526    
527    
528        return lines;
529      }
530    
531    
532    
533      /**
534       * Reads the DN of the entry from the provided list of lines.  The DN must be
535       * the first line in the list, unless the first line starts with "version",
536       * in which case the DN should be the second line.
537       *
538       * @param  lines  The set of lines from which the DN should be read.
539       *
540       * @return  The decoded entry DN.
541       *
542       * @throws  LDIFException  If DN is not the first element in the list (or the
543       *                         second after the LDIF version), or if a problem
544       *                         occurs while trying to parse it.
545       */
546      private DN readDN(LinkedList<StringBuilder> lines)
547              throws LDIFException
548      {
549        if (lines.isEmpty())
550        {
551          // This is possible if the contents of the first "entry" were just
552          // the version identifier.  If that is the case, then return null and
553          // use that as a signal to the caller to go ahead and read the next entry.
554          return null;
555        }
556    
557        StringBuilder line = lines.remove();
558        lastEntryHeaderLines.add(line);
559        int colonPos = line.indexOf(":");
560        if (colonPos <= 0)
561        {
562          Message message =
563                  ERR_LDIF_NO_ATTR_NAME.get(lastEntryLineNumber, line.toString());
564    
565          logToRejectWriter(lines, message);
566    
567          throw new LDIFException(message, lastEntryLineNumber, true);
568        }
569    
570        String attrName = toLowerCase(line.substring(0, colonPos));
571        if (attrName.equals("version"))
572        {
573          // This is the version line, and we can skip it.
574          return readDN(lines);
575        }
576        else if (! attrName.equals("dn"))
577        {
578          Message message =
579                  ERR_LDIF_NO_DN.get(lastEntryLineNumber, line.toString());
580    
581          logToRejectWriter(lines, message);
582    
583          throw new LDIFException(message, lastEntryLineNumber, true);
584        }
585    
586    
587        // Look at the character immediately after the colon.  If there is none,
588        // then assume the null DN.  If it is another colon, then the DN must be
589        // base64-encoded.  Otherwise, it may be one or more spaces.
590        int length = line.length();
591        if (colonPos == (length-1))
592        {
593          return DN.nullDN();
594        }
595    
596        if (line.charAt(colonPos+1) == ':')
597        {
598          // The DN is base64-encoded.  Find the first non-blank character and
599          // take the rest of the line, base64-decode it, and parse it as a DN.
600          int pos = colonPos+2;
601          while ((pos < length) && (line.charAt(pos) == ' '))
602          {
603            pos++;
604          }
605    
606          String encodedDNStr = line.substring(pos);
607    
608          String dnStr;
609          try
610          {
611            dnStr = new String(Base64.decode(encodedDNStr), "UTF-8");
612          }
613          catch (Exception e)
614          {
615            // The value did not have a valid base64-encoding.
616            if (debugEnabled())
617            {
618              TRACER.debugCaught(DebugLogLevel.ERROR, e);
619            }
620    
621            Message message =
622                    ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get(
623                            lastEntryLineNumber, line,
624                            String.valueOf(e));
625    
626            logToRejectWriter(lines, message);
627    
628            throw new LDIFException(message, lastEntryLineNumber, true, e);
629          }
630    
631          try
632          {
633            return DN.decode(dnStr);
634          }
635          catch (DirectoryException de)
636          {
637            if (debugEnabled())
638            {
639              TRACER.debugCaught(DebugLogLevel.ERROR, de);
640            }
641    
642            Message message = ERR_LDIF_INVALID_DN.get(
643                    lastEntryLineNumber, line.toString(),
644                    de.getMessageObject());
645    
646            logToRejectWriter(lines, message);
647    
648            throw new LDIFException(message, lastEntryLineNumber, true, de);
649          }
650          catch (Exception e)
651          {
652            if (debugEnabled())
653            {
654              TRACER.debugCaught(DebugLogLevel.ERROR, e);
655            }
656    
657            Message message = ERR_LDIF_INVALID_DN.get(
658                    lastEntryLineNumber, line.toString(),
659                    String.valueOf(e));
660    
661            logToRejectWriter(lines, message);
662    
663            throw new LDIFException(message, lastEntryLineNumber, true, e);
664          }
665        }
666        else
667        {
668          // The rest of the value should be the DN.  Skip over any spaces and
669          // attempt to decode the rest of the line as the DN.
670          int pos = colonPos+1;
671          while ((pos < length) && (line.charAt(pos) == ' '))
672          {
673            pos++;
674          }
675    
676          String dnString = line.substring(pos);
677    
678          try
679          {
680            return DN.decode(dnString);
681          }
682          catch (DirectoryException de)
683          {
684            if (debugEnabled())
685            {
686              TRACER.debugCaught(DebugLogLevel.ERROR, de);
687            }
688    
689            Message message = ERR_LDIF_INVALID_DN.get(
690                    lastEntryLineNumber, line.toString(), de.getMessageObject());
691    
692            logToRejectWriter(lines, message);
693    
694            throw new LDIFException(message, lastEntryLineNumber, true, de);
695          }
696          catch (Exception e)
697          {
698            if (debugEnabled())
699            {
700              TRACER.debugCaught(DebugLogLevel.ERROR, e);
701            }
702    
703            Message message = ERR_LDIF_INVALID_DN.get(
704                    lastEntryLineNumber, line.toString(),
705                    String.valueOf(e));
706    
707            logToRejectWriter(lines, message);
708    
709            throw new LDIFException(message, lastEntryLineNumber, true, e);
710          }
711        }
712      }
713    
714    
715    
716      /**
717       * Reads the changetype of the entry from the provided list of lines.  If
718       * there is no changetype attribute then an add is assumed.
719       *
720       * @param  lines  The set of lines from which the DN should be read.
721       *
722       * @return  The decoded entry DN.
723       *
724       * @throws  LDIFException  If DN is not the first element in the list (or the
725       *                         second after the LDIF version), or if a problem
726       *                         occurs while trying to parse it.
727       */
728      private String readChangeType(LinkedList<StringBuilder> lines)
729              throws LDIFException
730      {
731        if (lines.isEmpty())
732        {
733          // Error. There must be other entries.
734          return null;
735        }
736    
737        StringBuilder line = lines.get(0);
738        lastEntryHeaderLines.add(line);
739        int colonPos = line.indexOf(":");
740        if (colonPos <= 0)
741        {
742          Message message = ERR_LDIF_NO_ATTR_NAME.get(
743                  lastEntryLineNumber, line.toString());
744          logToRejectWriter(lines, message);
745          throw new LDIFException(message, lastEntryLineNumber, true);
746        }
747    
748        String attrName = toLowerCase(line.substring(0, colonPos));
749        if (! attrName.equals("changetype"))
750        {
751          // No changetype attribute - return null
752          return null;
753        } else
754        {
755          // Remove the line
756          lines.remove();
757        }
758    
759    
760        // Look at the character immediately after the colon.  If there is none,
761        // then no value was specified. Throw an exception
762        int length = line.length();
763        if (colonPos == (length-1))
764        {
765          Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get(
766              null, "add, delete, modify, moddn, modrdn");
767          throw new LDIFException(message, lastEntryLineNumber, false );
768        }
769    
770        if (line.charAt(colonPos+1) == ':')
771        {
772          // The change type is base64-encoded.  Find the first non-blank
773          // character and
774          // take the rest of the line, and base64-decode it.
775          int pos = colonPos+2;
776          while ((pos < length) && (line.charAt(pos) == ' '))
777          {
778            pos++;
779          }
780    
781          String encodedChangeTypeStr = line.substring(pos);
782    
783          String changeTypeStr;
784          try
785          {
786            changeTypeStr = new String(Base64.decode(encodedChangeTypeStr),
787                "UTF-8");
788          }
789          catch (Exception e)
790          {
791            // The value did not have a valid base64-encoding.
792            if (debugEnabled())
793            {
794              TRACER.debugCaught(DebugLogLevel.ERROR, e);
795            }
796    
797            Message message = ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get(
798                    lastEntryLineNumber, line,
799                    String.valueOf(e));
800            logToRejectWriter(lines, message);
801            throw new LDIFException(message, lastEntryLineNumber, true, e);
802          }
803    
804          return changeTypeStr;
805        }
806        else
807        {
808          // The rest of the value should be the changetype.
809          // Skip over any spaces and
810          // attempt to decode the rest of the line as the changetype string.
811          int pos = colonPos+1;
812          while ((pos < length) && (line.charAt(pos) == ' '))
813          {
814            pos++;
815          }
816    
817          String changeTypeString = line.substring(pos);
818    
819          return changeTypeString;
820        }
821      }
822    
823    
824      /**
825       * Decodes the provided line as an LDIF attribute and adds it to the
826       * appropriate hash.
827       *
828       * @param  lines                  The full set of lines that comprise the
829       *                                entry (used for writing reject information).
830       * @param  line                   The line to decode.
831       * @param  entryDN                The DN of the entry being decoded.
832       * @param  objectClasses          The set of objectclasses decoded so far for
833       *                                the current entry.
834       * @param  userAttributes         The set of user attributes decoded so far
835       *                                for the current entry.
836       * @param  operationalAttributes  The set of operational attributes decoded so
837       *                                far for the current entry.
838       * @param  checkSchema            Indicates whether to perform schema
839       *                                validation for the attribute.
840       *
841       * @throws  LDIFException  If a problem occurs while trying to decode the
842       *                         attribute contained in the provided entry.
843       */
844      private void readAttribute(LinkedList<StringBuilder> lines,
845           StringBuilder line, DN entryDN,
846           HashMap<ObjectClass,String> objectClasses,
847           HashMap<AttributeType,List<Attribute>> userAttributes,
848           HashMap<AttributeType,List<Attribute>> operationalAttributes,
849           boolean checkSchema)
850              throws LDIFException
851      {
852        // Parse the attribute type description.
853        int colonPos = parseColonPosition(lines, line);
854        String attrDescr = line.substring(0, colonPos);
855        Attribute attribute = parseAttrDescription(attrDescr);
856        String attrName = attribute.getName();
857        String lowerName = toLowerCase(attrName);
858        LinkedHashSet<String> options = attribute.getOptions();
859    
860        // Now parse the attribute value.
861        ASN1OctetString value = parseSingleValue(lines, line, entryDN,
862            colonPos, attrName);
863    
864        // See if this is an objectclass or an attribute.  Then get the
865        // corresponding definition and add the value to the appropriate hash.
866        if (lowerName.equals("objectclass"))
867        {
868          if (! importConfig.includeObjectClasses())
869          {
870            if (debugEnabled())
871            {
872              TRACER.debugVerbose("Skipping objectclass %s for entry %s due to " +
873                  "the import configuration.", value, entryDN);
874            }
875            return;
876          }
877    
878          String ocName      = value.stringValue();
879          String lowerOCName = toLowerCase(ocName);
880    
881          ObjectClass objectClass = DirectoryServer.getObjectClass(lowerOCName);
882          if (objectClass == null)
883          {
884            objectClass = DirectoryServer.getDefaultObjectClass(ocName);
885          }
886    
887          if (objectClasses.containsKey(objectClass))
888          {
889            logError(WARN_LDIF_DUPLICATE_OBJECTCLASS.get(
890                String.valueOf(entryDN), lastEntryLineNumber, ocName));
891          }
892          else
893          {
894            objectClasses.put(objectClass, ocName);
895          }
896        }
897        else
898        {
899          AttributeType attrType = DirectoryServer.getAttributeType(lowerName);
900          if (attrType == null)
901          {
902            attrType = DirectoryServer.getDefaultAttributeType(attrName);
903          }
904    
905    
906          if (! importConfig.includeAttribute(attrType))
907          {
908            if (debugEnabled())
909            {
910              TRACER.debugVerbose("Skipping attribute %s for entry %s due to the " +
911                  "import configuration.", attrName, entryDN);
912            }
913            return;
914          }
915    
916          if (checkSchema &&
917              (DirectoryServer.getSyntaxEnforcementPolicy() !=
918                   AcceptRejectWarn.ACCEPT))
919          {
920            MessageBuilder invalidReason = new MessageBuilder();
921            if (! attrType.getSyntax().valueIsAcceptable(value, invalidReason))
922            {
923              Message message = WARN_LDIF_VALUE_VIOLATES_SYNTAX.get(
924                      String.valueOf(entryDN),
925                      lastEntryLineNumber, value.stringValue(),
926                      attrName, invalidReason.toString());
927              if (DirectoryServer.getSyntaxEnforcementPolicy() ==
928                       AcceptRejectWarn.WARN)
929              {
930                logError(message);
931              }
932              else
933              {
934                logToRejectWriter(lines, message);
935                throw new LDIFException(message, lastEntryLineNumber,
936                                        true);
937              }
938            }
939          }
940    
941          AttributeValue attributeValue = new AttributeValue(attrType, value);
942          List<Attribute> attrList;
943          if (attrType.isOperational())
944          {
945            attrList = operationalAttributes.get(attrType);
946            if (attrList == null)
947            {
948              LinkedHashSet<AttributeValue> valueSet =
949                   new LinkedHashSet<AttributeValue>();
950              valueSet.add(attributeValue);
951    
952              attrList = new ArrayList<Attribute>();
953              attrList.add(new Attribute(attrType, attrName, options, valueSet));
954              operationalAttributes.put(attrType, attrList);
955              return;
956            }
957          }
958          else
959          {
960            attrList = userAttributes.get(attrType);
961            if (attrList == null)
962            {
963              LinkedHashSet<AttributeValue> valueSet =
964                   new LinkedHashSet<AttributeValue>();
965              valueSet.add(attributeValue);
966    
967              attrList = new ArrayList<Attribute>();
968              attrList.add(new Attribute(attrType, attrName, options, valueSet));
969              userAttributes.put(attrType, attrList);
970              return;
971            }
972          }
973    
974    
975          // Check to see if any of the attributes in the list have the same set of
976          // options.  If so, then try to add a value to that attribute.
977          for (Attribute a : attrList)
978          {
979            if (a.optionsEqual(options))
980            {
981              LinkedHashSet<AttributeValue> valueSet = a.getValues();
982              if (valueSet.contains(attributeValue))
983              {
984                if (! checkSchema)
985                {
986                  // If we're not doing schema checking, then it is possible that
987                  // the attribute type should use case-sensitive matching and the
988                  // values differ in capitalization.  Only reject the proposed
989                  // value if we find another value that is exactly the same as the
990                  // one that was provided.
991                  for (AttributeValue v : valueSet)
992                  {
993                    if (v.getValue().equals(attributeValue.getValue()))
994                    {
995                      Message message = WARN_LDIF_DUPLICATE_ATTR.get(
996                              String.valueOf(entryDN),
997                              lastEntryLineNumber, attrName,
998                              value.stringValue());
999                      logToRejectWriter(lines, message);
1000                      throw new LDIFException(message, lastEntryLineNumber,
1001                                              true);
1002                    }
1003                  }
1004                }
1005                else
1006                {
1007                  Message message = WARN_LDIF_DUPLICATE_ATTR.get(
1008                          String.valueOf(entryDN),
1009                          lastEntryLineNumber, attrName,
1010                          value.stringValue());
1011                  logToRejectWriter(lines, message);
1012                  throw new LDIFException(message, lastEntryLineNumber,
1013                                          true);
1014                }
1015              }
1016    
1017              if (attrType.isSingleValue() && (! valueSet.isEmpty()) && checkSchema)
1018              {
1019                Message message = ERR_LDIF_MULTIPLE_VALUES_FOR_SINGLE_VALUED_ATTR
1020                        .get(String.valueOf(entryDN),
1021                             lastEntryLineNumber, attrName);
1022                logToRejectWriter(lines, message);
1023                throw new LDIFException(message, lastEntryLineNumber, true);
1024              }
1025    
1026              valueSet.add(attributeValue);
1027              return;
1028            }
1029          }
1030    
1031    
1032          // No set of matching options was found, so create a new one and add it to
1033          // the list.
1034          LinkedHashSet<AttributeValue> valueSet =
1035               new LinkedHashSet<AttributeValue>();
1036          valueSet.add(attributeValue);
1037          attrList.add(new Attribute(attrType, attrName, options, valueSet));
1038          return;
1039        }
1040      }
1041    
1042    
1043    
1044      /**
1045       * Decodes the provided line as an LDIF attribute and returns the
1046       * Attribute (name and values) for the specified attribute name.
1047       *
1048       * @param  lines                  The full set of lines that comprise the
1049       *                                entry (used for writing reject information).
1050       * @param  line                   The line to decode.
1051       * @param  entryDN                The DN of the entry being decoded.
1052       * @param  attributeName          The name and options of the attribute to
1053       *                                return the values for.
1054       *
1055       * @return                        The attribute in octet string form.
1056       * @throws  LDIFException         If a problem occurs while trying to decode
1057       *                                the attribute contained in the provided
1058       *                                entry or if the parsed attribute name does
1059       *                                not match the specified attribute name.
1060       */
1061      private Attribute readSingleValueAttribute(
1062           LinkedList<StringBuilder> lines, StringBuilder line, DN entryDN,
1063           String attributeName) throws LDIFException
1064      {
1065        // Parse the attribute type description.
1066        int colonPos = parseColonPosition(lines, line);
1067        String attrDescr = line.substring(0, colonPos);
1068        Attribute attribute = parseAttrDescription(attrDescr);
1069        String attrName = attribute.getName();
1070    
1071        if (attributeName != null)
1072        {
1073          Attribute expectedAttr = parseAttrDescription(attributeName);
1074    
1075          if (!attribute.equals(expectedAttr))
1076          {
1077            Message message = ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE.get(
1078                attrDescr, attributeName);
1079            throw new LDIFException(message, lastEntryLineNumber, false);
1080          }
1081        }
1082    
1083        //  Now parse the attribute value.
1084        ASN1OctetString value = parseSingleValue(lines, line, entryDN,
1085            colonPos, attrName);
1086    
1087        AttributeType attrType = attribute.getAttributeType();
1088        AttributeValue attributeValue = new AttributeValue(attrType, value);
1089        attribute.getValues().add(attributeValue);
1090    
1091        return attribute;
1092      }
1093    
1094    
1095      /**
1096       * Retrieves the starting line number for the last entry read from the LDIF
1097       * source.
1098       *
1099       * @return  The starting line number for the last entry read from the LDIF
1100       *          source.
1101       */
1102      public long getLastEntryLineNumber()
1103      {
1104        return lastEntryLineNumber;
1105      }
1106    
1107    
1108    
1109      /**
1110       * Rejects the last entry read from the LDIF.  This method is intended for use
1111       * by components that perform their own validation of entries (e.g., backends
1112       * during import processing) in which the entry appeared valid to the LDIF
1113       * reader but some other problem was encountered.
1114       *
1115       * @param  message  A human-readable message providing the reason that the
1116       *                  last entry read was not acceptable.
1117       */
1118      public void rejectLastEntry(Message message)
1119      {
1120        entriesRejected++;
1121    
1122        BufferedWriter rejectWriter = importConfig.getRejectWriter();
1123        if (rejectWriter != null)
1124        {
1125          try
1126          {
1127            if ((message != null) && (message.length() > 0))
1128            {
1129              rejectWriter.write("# ");
1130              rejectWriter.write(message.toString());
1131              rejectWriter.newLine();
1132            }
1133    
1134            for (StringBuilder sb : lastEntryHeaderLines)
1135            {
1136              rejectWriter.write(sb.toString());
1137              rejectWriter.newLine();
1138            }
1139    
1140            for (StringBuilder sb : lastEntryBodyLines)
1141            {
1142              rejectWriter.write(sb.toString());
1143              rejectWriter.newLine();
1144            }
1145    
1146            rejectWriter.newLine();
1147          }
1148          catch (Exception e)
1149          {
1150            if (debugEnabled())
1151            {
1152              TRACER.debugCaught(DebugLogLevel.ERROR, e);
1153            }
1154          }
1155        }
1156      }
1157    
1158    
1159    
1160      /**
1161       * Closes this LDIF reader and the underlying file or input stream.
1162       */
1163      public void close()
1164      {
1165        importConfig.close();
1166      }
1167    
1168    
1169    
1170      /**
1171       * Parse an AttributeDescription (an attribute type name and its options).
1172       * @param attrDescr The attribute description to be parsed.
1173       * @return A new attribute with no values, representing the attribute type
1174       * and its options.
1175       */
1176      private static Attribute parseAttrDescription(String attrDescr)
1177      {
1178        String attrName;
1179        String lowerName;
1180        LinkedHashSet<String> options;
1181        int semicolonPos = attrDescr.indexOf(';');
1182        if (semicolonPos > 0)
1183        {
1184          attrName = attrDescr.substring(0, semicolonPos);
1185          options = new LinkedHashSet<String>();
1186          int nextPos = attrDescr.indexOf(';', semicolonPos+1);
1187          while (nextPos > 0)
1188          {
1189            String option = attrDescr.substring(semicolonPos+1, nextPos);
1190            if (option.length() > 0)
1191            {
1192              options.add(option);
1193              semicolonPos = nextPos;
1194              nextPos = attrDescr.indexOf(';', semicolonPos+1);
1195            }
1196          }
1197    
1198          String option = attrDescr.substring(semicolonPos+1);
1199          if (option.length() > 0)
1200          {
1201            options.add(option);
1202          }
1203        }
1204        else
1205        {
1206          attrName  = attrDescr;
1207          options   = null;
1208        }
1209    
1210        lowerName = toLowerCase(attrName);
1211        AttributeType attrType = DirectoryServer.getAttributeType(lowerName);
1212        if (attrType == null)
1213        {
1214          attrType = DirectoryServer.getDefaultAttributeType(attrName);
1215        }
1216    
1217        return new Attribute(attrType, attrName, options, null);
1218      }
1219    
1220    
1221    
1222      /**
1223       * Retrieves the total number of entries read so far by this LDIF reader,
1224       * including those that have been ignored or rejected.
1225       *
1226       * @return  The total number of entries read so far by this LDIF reader.
1227       */
1228      public long getEntriesRead()
1229      {
1230        return entriesRead;
1231      }
1232    
1233    
1234    
1235      /**
1236       * Retrieves the total number of entries that have been ignored so far by this
1237       * LDIF reader because they did not match the import criteria.
1238       *
1239       * @return  The total number of entries ignored so far by this LDIF reader.
1240       */
1241      public long getEntriesIgnored()
1242      {
1243        return entriesIgnored;
1244      }
1245    
1246    
1247    
1248      /**
1249       * Retrieves the total number of entries rejected so far by this LDIF reader.
1250       * This  includes both entries that were rejected because  of internal
1251       * validation failure (e.g., they didn't conform to the defined  server
1252       * schema) or an external validation failure (e.g., the component using this
1253       * LDIF reader didn't accept the entry because it didn't have a parent).
1254       *
1255       * @return  The total number of entries rejected so far by this LDIF reader.
1256       */
1257      public long getEntriesRejected()
1258      {
1259        return entriesRejected;
1260      }
1261    
1262    
1263    
1264      /**
1265       * Parse a modifyDN change record entry from LDIF.
1266       *
1267       * @param entryDN
1268       *          The name of the entry being modified.
1269       * @param lines
1270       *          The lines to parse.
1271       * @return Returns the parsed modifyDN change record entry.
1272       * @throws LDIFException
1273       *           If there was an error when parsing the change record.
1274       */
1275      private ChangeRecordEntry parseModifyDNChangeRecordEntry(DN entryDN,
1276          LinkedList<StringBuilder> lines) throws LDIFException {
1277    
1278        DN newSuperiorDN = null;
1279        RDN newRDN = null;
1280        boolean deleteOldRDN = false;
1281    
1282        if(lines.isEmpty())
1283        {
1284          Message message = ERR_LDIF_NO_MOD_DN_ATTRIBUTES.get();
1285          throw new LDIFException(message, lineNumber, true);
1286        }
1287    
1288        StringBuilder line = lines.remove();
1289        String rdnStr = getModifyDNAttributeValue(lines, line, entryDN, "newrdn");
1290    
1291        try
1292        {
1293          newRDN = RDN.decode(rdnStr);
1294        } catch (DirectoryException de)
1295        {
1296          if (debugEnabled())
1297          {
1298            TRACER.debugCaught(DebugLogLevel.ERROR, de);
1299          }
1300          Message message = ERR_LDIF_INVALID_DN.get(
1301              lineNumber, line.toString(), de.getMessageObject());
1302          throw new LDIFException(message, lineNumber, true);
1303        } catch (Exception e)
1304        {
1305          if (debugEnabled())
1306          {
1307            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1308          }
1309          Message message =
1310              ERR_LDIF_INVALID_DN.get(lineNumber, line.toString(), e.getMessage());
1311          throw new LDIFException(message, lineNumber, true);
1312        }
1313    
1314        if(lines.isEmpty())
1315        {
1316          Message message = ERR_LDIF_NO_DELETE_OLDRDN_ATTRIBUTE.get();
1317          throw new LDIFException(message, lineNumber, true);
1318        }
1319        lineNumber++;
1320    
1321        line = lines.remove();
1322        String delStr = getModifyDNAttributeValue(lines, line,
1323            entryDN, "deleteoldrdn");
1324    
1325        if(delStr.equalsIgnoreCase("false") ||
1326            delStr.equalsIgnoreCase("no") ||
1327            delStr.equalsIgnoreCase("0"))
1328        {
1329          deleteOldRDN = false;
1330        } else if(delStr.equalsIgnoreCase("true") ||
1331            delStr.equalsIgnoreCase("yes") ||
1332            delStr.equalsIgnoreCase("1"))
1333        {
1334          deleteOldRDN = true;
1335        } else
1336        {
1337          Message message = ERR_LDIF_INVALID_DELETE_OLDRDN_ATTRIBUTE.get(delStr);
1338          throw new LDIFException(message, lineNumber, true);
1339        }
1340    
1341        if(!lines.isEmpty())
1342        {
1343          lineNumber++;
1344    
1345          line = lines.remove();
1346    
1347          String dnStr = getModifyDNAttributeValue(lines, line,
1348              entryDN, "newsuperior");
1349          try
1350          {
1351            newSuperiorDN = DN.decode(dnStr);
1352          } catch (DirectoryException de)
1353          {
1354            if (debugEnabled())
1355            {
1356              TRACER.debugCaught(DebugLogLevel.ERROR, de);
1357            }
1358            Message message = ERR_LDIF_INVALID_DN.get(
1359                lineNumber, line.toString(), de.getMessageObject());
1360            throw new LDIFException(message, lineNumber, true);
1361          } catch (Exception e)
1362          {
1363            if (debugEnabled())
1364            {
1365              TRACER.debugCaught(DebugLogLevel.ERROR, e);
1366            }
1367            Message message = ERR_LDIF_INVALID_DN.get(
1368                lineNumber, line.toString(), e.getMessage());
1369            throw new LDIFException(message, lineNumber, true);
1370          }
1371        }
1372    
1373        return new ModifyDNChangeRecordEntry(entryDN, newRDN, deleteOldRDN,
1374                                             newSuperiorDN);
1375      }
1376    
1377    
1378    
1379      /**
1380       * Return the string value for the specified attribute name which only
1381       * has one value.
1382       *
1383       * @param lines
1384       *          The set of lines for this change record entry.
1385       * @param line
1386       *          The line currently being examined.
1387       * @param entryDN
1388       *          The name of the entry being modified.
1389       * @param attributeName
1390       *          The attribute name
1391       * @return the string value for the attribute name.
1392       * @throws LDIFException
1393       *           If a problem occurs while attempting to determine the
1394       *           attribute value.
1395       */
1396    
1397      private String getModifyDNAttributeValue(LinkedList<StringBuilder> lines,
1398                                       StringBuilder line,
1399                                       DN entryDN,
1400                                       String attributeName) throws LDIFException
1401      {
1402        Attribute attr =
1403          readSingleValueAttribute(lines, line, entryDN, attributeName);
1404        LinkedHashSet<AttributeValue> values = attr.getValues();
1405    
1406        // Get the attribute value
1407        Object[] vals = values.toArray();
1408        return (((AttributeValue)vals[0]).getStringValue());
1409      }
1410    
1411    
1412    
1413      /**
1414       * Parse a modify change record entry from LDIF.
1415       *
1416       * @param entryDN
1417       *          The name of the entry being modified.
1418       * @param lines
1419       *          The lines to parse.
1420       * @return Returns the parsed modify change record entry.
1421       * @throws LDIFException
1422       *           If there was an error when parsing the change record.
1423       */
1424      private ChangeRecordEntry parseModifyChangeRecordEntry(DN entryDN,
1425          LinkedList<StringBuilder> lines) throws LDIFException {
1426    
1427        List<RawModification> modifications = new ArrayList<RawModification>();
1428        while(!lines.isEmpty())
1429        {
1430          ModificationType modType = null;
1431    
1432          StringBuilder line = lines.remove();
1433          Attribute attr =
1434            readSingleValueAttribute(lines, line, entryDN, null);
1435          String name = attr.getName();
1436          LinkedHashSet<AttributeValue> values = attr.getValues();
1437    
1438          // Get the attribute description
1439          String attrDescr = values.iterator().next().getStringValue();
1440    
1441          String lowerName = toLowerCase(name);
1442          if(lowerName.equals("add"))
1443          {
1444            modType = ModificationType.ADD;
1445          } else if(lowerName.equals("delete"))
1446          {
1447            modType = ModificationType.DELETE;
1448          } else if(lowerName.equals("replace"))
1449          {
1450            modType = ModificationType.REPLACE;
1451          } else if(lowerName.equals("increment"))
1452          {
1453            modType = ModificationType.INCREMENT;
1454          } else
1455          {
1456            // Invalid attribute name.
1457            Message message = ERR_LDIF_INVALID_MODIFY_ATTRIBUTE.get(
1458                name, "add, delete, replace, increment");
1459            throw new LDIFException(message, lineNumber, true);
1460          }
1461    
1462          // Now go through the rest of the attributes till the "-" line is
1463          // reached.
1464          Attribute modAttr = LDIFReader.parseAttrDescription(attrDescr);
1465          while (! lines.isEmpty())
1466          {
1467            line = lines.remove();
1468            if(line.toString().equals("-"))
1469            {
1470              break;
1471            }
1472            Attribute a =
1473              readSingleValueAttribute(lines, line, entryDN, attrDescr);
1474            modAttr.getValues().addAll(a.getValues());
1475          }
1476    
1477          LDAPAttribute ldapAttr = new LDAPAttribute(modAttr);
1478          LDAPModification mod = new LDAPModification(modType, ldapAttr);
1479          modifications.add(mod);
1480        }
1481    
1482        return new ModifyChangeRecordEntry(entryDN, modifications);
1483      }
1484    
1485    
1486    
1487      /**
1488       * Parse a delete change record entry from LDIF.
1489       *
1490       * @param entryDN
1491       *          The name of the entry being deleted.
1492       * @param lines
1493       *          The lines to parse.
1494       * @return Returns the parsed delete change record entry.
1495       * @throws LDIFException
1496       *           If there was an error when parsing the change record.
1497       */
1498      private ChangeRecordEntry parseDeleteChangeRecordEntry(DN entryDN,
1499          LinkedList<StringBuilder> lines) throws LDIFException {
1500    
1501        if (!lines.isEmpty())
1502        {
1503          Message message = ERR_LDIF_INVALID_DELETE_ATTRIBUTES.get();
1504          throw new LDIFException(message, lineNumber, true);
1505        }
1506    
1507        return new DeleteChangeRecordEntry(entryDN);
1508      }
1509    
1510    
1511    
1512      /**
1513       * Parse an add change record entry from LDIF.
1514       *
1515       * @param entryDN
1516       *          The name of the entry being added.
1517       * @param lines
1518       *          The lines to parse.
1519       * @return Returns the parsed add change record entry.
1520       * @throws LDIFException
1521       *           If there was an error when parsing the change record.
1522       */
1523      private ChangeRecordEntry parseAddChangeRecordEntry(DN entryDN,
1524          LinkedList<StringBuilder> lines) throws LDIFException {
1525    
1526        HashMap<ObjectClass,String> objectClasses =
1527          new HashMap<ObjectClass,String>();
1528        HashMap<AttributeType,List<Attribute>> attributes =
1529          new HashMap<AttributeType, List<Attribute>>();
1530        for(StringBuilder line : lines)
1531        {
1532          readAttribute(lines, line, entryDN, objectClasses,
1533              attributes, attributes, importConfig.validateSchema());
1534        }
1535    
1536        // Reconstruct the object class attribute.
1537        AttributeType ocType = DirectoryServer.getObjectClassAttributeType();
1538        LinkedHashSet<AttributeValue> ocValues =
1539          new LinkedHashSet<AttributeValue>(objectClasses.size());
1540        for (String value : objectClasses.values()) {
1541          AttributeValue av = new AttributeValue(ocType, value);
1542          ocValues.add(av);
1543        }
1544        Attribute ocAttr = new Attribute(ocType, "objectClass", ocValues);
1545        List<Attribute> ocAttrList = new ArrayList<Attribute>(1);
1546        ocAttrList.add(ocAttr);
1547        attributes.put(ocType, ocAttrList);
1548    
1549        return new AddChangeRecordEntry(entryDN, attributes);
1550      }
1551    
1552    
1553    
1554      /**
1555       * Parse colon position in an attribute description.
1556       *
1557       * @param lines
1558       *          The current set of lines.
1559       * @param line
1560       *          The current line.
1561       * @return The colon position.
1562       * @throws LDIFException
1563       *           If the colon was badly placed or not found.
1564       */
1565      private int parseColonPosition(LinkedList<StringBuilder> lines,
1566          StringBuilder line) throws LDIFException {
1567    
1568        int colonPos = line.indexOf(":");
1569        if (colonPos <= 0)
1570        {
1571          Message message = ERR_LDIF_NO_ATTR_NAME.get(
1572                  lastEntryLineNumber, line.toString());
1573          logToRejectWriter(lines, message);
1574          throw new LDIFException(message, lastEntryLineNumber, true);
1575        }
1576        return colonPos;
1577      }
1578    
1579    
1580    
1581      /**
1582       * Parse a single attribute value from a line of LDIF.
1583       *
1584       * @param lines
1585       *          The current set of lines.
1586       * @param line
1587       *          The current line.
1588       * @param entryDN
1589       *          The DN of the entry being parsed.
1590       * @param colonPos
1591       *          The position of the separator colon in the line.
1592       * @param attrName
1593       *          The name of the attribute being parsed.
1594       * @return The parsed attribute value.
1595       * @throws LDIFException
1596       *           If an error occurred when parsing the attribute value.
1597       */
1598      private ASN1OctetString parseSingleValue(
1599          LinkedList<StringBuilder> lines,
1600          StringBuilder line,
1601          DN entryDN,
1602          int colonPos,
1603          String attrName) throws LDIFException {
1604    
1605        // Look at the character immediately after the colon. If there is
1606        // none, then assume an attribute with an empty value. If it is another
1607        // colon, then the value must be base64-encoded. If it is a less-than
1608        // sign, then assume that it is a URL. Otherwise, it is a regular value.
1609        int length = line.length();
1610        ASN1OctetString value;
1611        if (colonPos == (length-1))
1612        {
1613          value = new ASN1OctetString();
1614        }
1615        else
1616        {
1617          char c = line.charAt(colonPos+1);
1618          if (c == ':')
1619          {
1620            // The value is base64-encoded. Find the first non-blank
1621            // character, take the rest of the line, and base64-decode it.
1622            int pos = colonPos+2;
1623            while ((pos < length) && (line.charAt(pos) == ' '))
1624            {
1625              pos++;
1626            }
1627    
1628            try
1629            {
1630              value = new ASN1OctetString(Base64.decode(line.substring(pos)));
1631            }
1632            catch (Exception e)
1633            {
1634              // The value did not have a valid base64-encoding.
1635              if (debugEnabled())
1636              {
1637                TRACER.debugCaught(DebugLogLevel.ERROR, e);
1638              }
1639    
1640              Message message = ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR.get(
1641                      String.valueOf(entryDN),
1642                      lastEntryLineNumber, line,
1643                      String.valueOf(e));
1644              logToRejectWriter(lines, message);
1645              throw new LDIFException(message, lastEntryLineNumber, true, e);
1646            }
1647          }
1648          else if (c == '<')
1649          {
1650            // Find the first non-blank character, decode the rest of the
1651            // line as a URL, and read its contents.
1652            int pos = colonPos+2;
1653            while ((pos < length) && (line.charAt(pos) == ' '))
1654            {
1655              pos++;
1656            }
1657    
1658            URL contentURL;
1659            try
1660            {
1661              contentURL = new URL(line.substring(pos));
1662            }
1663            catch (Exception e)
1664            {
1665              // The URL was malformed or had an invalid protocol.
1666              if (debugEnabled())
1667              {
1668                TRACER.debugCaught(DebugLogLevel.ERROR, e);
1669              }
1670    
1671              Message message = ERR_LDIF_INVALID_URL.get(String.valueOf(entryDN),
1672                                          lastEntryLineNumber,
1673                                          String.valueOf(attrName),
1674                                          String.valueOf(e));
1675              logToRejectWriter(lines, message);
1676              throw new LDIFException(message, lastEntryLineNumber, true, e);
1677            }
1678    
1679    
1680            InputStream inputStream = null;
1681            ByteArrayOutputStream outputStream = null;
1682            try
1683            {
1684              outputStream = new ByteArrayOutputStream();
1685              inputStream  = contentURL.openConnection().getInputStream();
1686    
1687              int bytesRead;
1688              while ((bytesRead = inputStream.read(buffer)) > 0)
1689              {
1690                outputStream.write(buffer, 0, bytesRead);
1691              }
1692    
1693              value = new ASN1OctetString(outputStream.toByteArray());
1694            }
1695            catch (Exception e)
1696            {
1697              // We were unable to read the contents of that URL for some
1698              // reason.
1699              if (debugEnabled())
1700              {
1701                TRACER.debugCaught(DebugLogLevel.ERROR, e);
1702              }
1703    
1704              Message message = ERR_LDIF_URL_IO_ERROR.get(String.valueOf(entryDN),
1705                                          lastEntryLineNumber,
1706                                          String.valueOf(attrName),
1707                                          String.valueOf(contentURL),
1708                                          String.valueOf(e));
1709              logToRejectWriter(lines, message);
1710              throw new LDIFException(message, lastEntryLineNumber, true, e);
1711            }
1712            finally
1713            {
1714              if (outputStream != null)
1715              {
1716                try
1717                {
1718                  outputStream.close();
1719                } catch (Exception e) {}
1720              }
1721    
1722              if (inputStream != null)
1723              {
1724                try
1725                {
1726                  inputStream.close();
1727                } catch (Exception e) {}
1728              }
1729            }
1730          }
1731          else
1732          {
1733            // The rest of the line should be the value. Skip over any
1734            // spaces and take the rest of the line as the value.
1735            int pos = colonPos+1;
1736            while ((pos < length) && (line.charAt(pos) == ' '))
1737            {
1738              pos++;
1739            }
1740    
1741            value = new ASN1OctetString(line.substring(pos));
1742          }
1743        }
1744        return value;
1745      }
1746    
1747      /**
1748       * Log a message to the reject writer if one is configured.
1749       *
1750       * @param lines
1751       *          The set of rejected lines.
1752       * @param message
1753       *          The associated error message.
1754       */
1755      private void logToRejectWriter(LinkedList<StringBuilder> lines,
1756          Message message) {
1757    
1758        BufferedWriter rejectWriter = importConfig.getRejectWriter();
1759        if (rejectWriter != null)
1760        {
1761          logToWriter(rejectWriter, lines, message);
1762        }
1763      }
1764    
1765      /**
1766       * Log a message to the reject writer if one is configured.
1767       *
1768       * @param lines
1769       *          The set of rejected lines.
1770       * @param message
1771       *          The associated error message.
1772       */
1773      private void logToSkipWriter(LinkedList<StringBuilder> lines,
1774          Message message) {
1775    
1776        BufferedWriter skipWriter = importConfig.getSkipWriter();
1777        if (skipWriter != null)
1778        {
1779          logToWriter(skipWriter, lines, message);
1780        }
1781      }
1782    
1783      /**
1784       * Log a message to the given writer.
1785       *
1786       * @param writer
1787       *          The writer to write to.
1788       * @param lines
1789     *          The set of rejected lines.
1790       * @param message
1791       *          The associated error message.
1792       */
1793      private void logToWriter(BufferedWriter writer,
1794          LinkedList<StringBuilder> lines,
1795          Message message)
1796      {
1797        if (writer != null)
1798        {
1799          try
1800          {
1801            writer.write("# ");
1802            writer.write(String.valueOf(message));
1803            writer.newLine();
1804            for (StringBuilder sb : lines)
1805            {
1806              writer.write(sb.toString());
1807              writer.newLine();
1808            }
1809    
1810            writer.newLine();
1811          }
1812          catch (Exception e)
1813          {
1814            if (debugEnabled())
1815            {
1816              TRACER.debugCaught(DebugLogLevel.ERROR, e);
1817            }
1818          }
1819        }
1820      }
1821    
1822    }
1823