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