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