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