001 /*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.ldap.sdk;
022
023
024
025 import java.io.Serializable;
026 import java.util.ArrayList;
027 import java.util.List;
028
029 import com.unboundid.asn1.ASN1Buffer;
030 import com.unboundid.asn1.ASN1BufferSequence;
031 import com.unboundid.asn1.ASN1BufferSet;
032 import com.unboundid.asn1.ASN1Element;
033 import com.unboundid.asn1.ASN1Enumerated;
034 import com.unboundid.asn1.ASN1Exception;
035 import com.unboundid.asn1.ASN1OctetString;
036 import com.unboundid.asn1.ASN1Sequence;
037 import com.unboundid.asn1.ASN1Set;
038 import com.unboundid.asn1.ASN1StreamReader;
039 import com.unboundid.asn1.ASN1StreamReaderSet;
040 import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
041 import com.unboundid.util.Base64;
042 import com.unboundid.util.NotMutable;
043 import com.unboundid.util.ThreadSafety;
044 import com.unboundid.util.ThreadSafetyLevel;
045
046 import static com.unboundid.ldap.sdk.LDAPMessages.*;
047 import static com.unboundid.util.Debug.*;
048 import static com.unboundid.util.StaticUtils.*;
049 import static com.unboundid.util.Validator.*;
050
051
052
053 /**
054 * This class provides a data structure for holding information about an LDAP
055 * modification, which describes a change to apply to an attribute. A
056 * modification includes the following elements:
057 * <UL>
058 * <LI>A modification type, which describes the type of change to apply.</LI>
059 * <LI>An attribute name, which specifies which attribute should be
060 * updated.</LI>
061 * <LI>An optional set of values to use for the modification.</LI>
062 * </UL>
063 */
064 @NotMutable()
065 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
066 public final class Modification
067 implements Serializable
068 {
069 /**
070 * The value array that will be used when the modification should not have any
071 * values.
072 */
073 private static final ASN1OctetString[] NO_VALUES = new ASN1OctetString[0];
074
075
076
077 /**
078 * The byte array value array that will be used when the modification does not
079 * have any values.
080 */
081 private static final byte[][] NO_BYTE_VALUES = new byte[0][];
082
083
084
085 /**
086 * The serial version UID for this serializable class.
087 */
088 private static final long serialVersionUID = 5170107037390858876L;
089
090
091
092 // The set of values for this modification.
093 private final ASN1OctetString[] values;
094
095 // The modification type for this modification.
096 private final ModificationType modificationType;
097
098 // The name of the attribute to target with this modification.
099 private final String attributeName;
100
101
102
103 /**
104 * Creates a new LDAP modification with the provided modification type and
105 * attribute name. It will not have any values.
106 *
107 * @param modificationType The modification type for this modification.
108 * @param attributeName The name of the attribute to target with this
109 * modification. It must not be {@code null}.
110 */
111 public Modification(final ModificationType modificationType,
112 final String attributeName)
113 {
114 ensureNotNull(attributeName);
115
116 this.modificationType = modificationType;
117 this.attributeName = attributeName;
118
119 values = NO_VALUES;
120 }
121
122
123
124 /**
125 * Creates a new LDAP modification with the provided information.
126 *
127 * @param modificationType The modification type for this modification.
128 * @param attributeName The name of the attribute to target with this
129 * modification. It must not be {@code null}.
130 * @param attributeValue The attribute value for this modification. It
131 * must not be {@code null}.
132 */
133 public Modification(final ModificationType modificationType,
134 final String attributeName, final String attributeValue)
135 {
136 ensureNotNull(attributeName, attributeValue);
137
138 this.modificationType = modificationType;
139 this.attributeName = attributeName;
140
141 values = new ASN1OctetString[] { new ASN1OctetString(attributeValue) };
142 }
143
144
145
146 /**
147 * Creates a new LDAP modification with the provided information.
148 *
149 * @param modificationType The modification type for this modification.
150 * @param attributeName The name of the attribute to target with this
151 * modification. It must not be {@code null}.
152 * @param attributeValue The attribute value for this modification. It
153 * must not be {@code null}.
154 */
155 public Modification(final ModificationType modificationType,
156 final String attributeName, final byte[] attributeValue)
157 {
158 ensureNotNull(attributeName, attributeValue);
159
160 this.modificationType = modificationType;
161 this.attributeName = attributeName;
162
163 values = new ASN1OctetString[] { new ASN1OctetString(attributeValue) };
164 }
165
166
167
168 /**
169 * Creates a new LDAP modification with the provided information.
170 *
171 * @param modificationType The modification type for this modification.
172 * @param attributeName The name of the attribute to target with this
173 * modification. It must not be {@code null}.
174 * @param attributeValues The set of attribute value for this modification.
175 * It must not be {@code null}.
176 */
177 public Modification(final ModificationType modificationType,
178 final String attributeName,
179 final String... attributeValues)
180 {
181 ensureNotNull(attributeName, attributeValues);
182
183 this.modificationType = modificationType;
184 this.attributeName = attributeName;
185
186 values = new ASN1OctetString[attributeValues.length];
187 for (int i=0; i < values.length; i++)
188 {
189 values[i] = new ASN1OctetString(attributeValues[i]);
190 }
191 }
192
193
194
195 /**
196 * Creates a new LDAP modification with the provided information.
197 *
198 * @param modificationType The modification type for this modification.
199 * @param attributeName The name of the attribute to target with this
200 * modification. It must not be {@code null}.
201 * @param attributeValues The set of attribute value for this modification.
202 * It must not be {@code null}.
203 */
204 public Modification(final ModificationType modificationType,
205 final String attributeName,
206 final byte[]... attributeValues)
207 {
208 ensureNotNull(attributeName, attributeValues);
209
210 this.modificationType = modificationType;
211 this.attributeName = attributeName;
212
213 values = new ASN1OctetString[attributeValues.length];
214 for (int i=0; i < values.length; i++)
215 {
216 values[i] = new ASN1OctetString(attributeValues[i]);
217 }
218 }
219
220
221
222 /**
223 * Creates a new LDAP modification with the provided information.
224 *
225 * @param modificationType The modification type for this modification.
226 * @param attributeName The name of the attribute to target with this
227 * modification. It must not be {@code null}.
228 * @param attributeValues The set of attribute value for this modification.
229 * It must not be {@code null}.
230 */
231 public Modification(final ModificationType modificationType,
232 final String attributeName,
233 final ASN1OctetString[] attributeValues)
234 {
235 this.modificationType = modificationType;
236 this.attributeName = attributeName;
237 values = attributeValues;
238 }
239
240
241
242 /**
243 * Retrieves the modification type for this modification.
244 *
245 * @return The modification type for this modification.
246 */
247 public ModificationType getModificationType()
248 {
249 return modificationType;
250 }
251
252
253
254 /**
255 * Retrieves the attribute for this modification.
256 *
257 * @return The attribute for this modification.
258 */
259 public Attribute getAttribute()
260 {
261 return new Attribute(attributeName,
262 CaseIgnoreStringMatchingRule.getInstance(), values);
263 }
264
265
266
267 /**
268 * Retrieves the name of the attribute to target with this modification.
269 *
270 * @return The name of the attribute to target with this modification.
271 */
272 public String getAttributeName()
273 {
274 return attributeName;
275 }
276
277
278
279 /**
280 * Indicates whether this modification has at least one value.
281 *
282 * @return {@code true} if this modification has one or more values, or
283 * {@code false} if not.
284 */
285 public boolean hasValue()
286 {
287 return (values.length > 0);
288 }
289
290
291
292 /**
293 * Retrieves the set of values for this modification as an array of strings.
294 *
295 * @return The set of values for this modification as an array of strings.
296 */
297 public String[] getValues()
298 {
299 if (values.length == 0)
300 {
301 return NO_STRINGS;
302 }
303 else
304 {
305 final String[] stringValues = new String[values.length];
306 for (int i=0; i < values.length; i++)
307 {
308 stringValues[i] = values[i].stringValue();
309 }
310
311 return stringValues;
312 }
313 }
314
315
316
317 /**
318 * Retrieves the set of values for this modification as an array of byte
319 * arrays.
320 *
321 * @return The set of values for this modification as an array of byte
322 * arrays.
323 */
324 public byte[][] getValueByteArrays()
325 {
326 if (values.length == 0)
327 {
328 return NO_BYTE_VALUES;
329 }
330 else
331 {
332 final byte[][] byteValues = new byte[values.length][];
333 for (int i=0; i < values.length; i++)
334 {
335 byteValues[i] = values[i].getValue();
336 }
337
338 return byteValues;
339 }
340 }
341
342
343
344 /**
345 * Retrieves the set of values for this modification as an array of ASN.1
346 * octet strings.
347 *
348 * @return The set of values for this modification as an array of ASN.1 octet
349 * strings.
350 */
351 public ASN1OctetString[] getRawValues()
352 {
353 return values;
354 }
355
356
357
358 /**
359 * Writes an ASN.1-encoded representation of this modification to the provided
360 * ASN.1 buffer.
361 *
362 * @param buffer The ASN.1 buffer to which the encoded representation should
363 * be written.
364 */
365 public void writeTo(final ASN1Buffer buffer)
366 {
367 final ASN1BufferSequence modSequence = buffer.beginSequence();
368 buffer.addEnumerated(modificationType.intValue());
369
370 final ASN1BufferSequence attrSequence = buffer.beginSequence();
371 buffer.addOctetString(attributeName);
372
373 final ASN1BufferSet valueSet = buffer.beginSet();
374 for (final ASN1OctetString v : values)
375 {
376 buffer.addElement(v);
377 }
378 valueSet.end();
379 attrSequence.end();
380 modSequence.end();
381 }
382
383
384
385 /**
386 * Encodes this modification to an ASN.1 sequence suitable for use in the LDAP
387 * protocol.
388 *
389 * @return An ASN.1 sequence containing the encoded value.
390 */
391 public ASN1Sequence encode()
392 {
393 final ASN1Element[] attrElements =
394 {
395 new ASN1OctetString(attributeName),
396 new ASN1Set(values)
397 };
398
399 final ASN1Element[] modificationElements =
400 {
401 new ASN1Enumerated(modificationType.intValue()),
402 new ASN1Sequence(attrElements)
403 };
404
405 return new ASN1Sequence(modificationElements);
406 }
407
408
409
410 /**
411 * Reads and decodes an LDAP modification from the provided ASN.1 stream
412 * reader.
413 *
414 * @param reader The ASN.1 stream reader from which to read the
415 * modification.
416 *
417 * @return The decoded modification.
418 *
419 * @throws LDAPException If a problem occurs while trying to read or decode
420 * the modification.
421 */
422 public static Modification readFrom(final ASN1StreamReader reader)
423 throws LDAPException
424 {
425 try
426 {
427 ensureNotNull(reader.beginSequence());
428 final ModificationType modType =
429 ModificationType.valueOf(reader.readEnumerated());
430
431 ensureNotNull(reader.beginSequence());
432 final String attrName = reader.readString();
433
434 final ArrayList<ASN1OctetString> valueList =
435 new ArrayList<ASN1OctetString>(5);
436 final ASN1StreamReaderSet valueSet = reader.beginSet();
437 while (valueSet.hasMoreElements())
438 {
439 valueList.add(new ASN1OctetString(reader.readBytes()));
440 }
441
442 final ASN1OctetString[] values = new ASN1OctetString[valueList.size()];
443 valueList.toArray(values);
444
445 return new Modification(modType, attrName, values);
446 }
447 catch (Exception e)
448 {
449 debugException(e);
450 throw new LDAPException(ResultCode.DECODING_ERROR,
451 ERR_MOD_CANNOT_DECODE.get(getExceptionMessage(e)), e);
452 }
453 }
454
455
456
457 /**
458 * Decodes the provided ASN.1 sequence as an LDAP modification.
459 *
460 * @param modificationSequence The ASN.1 sequence to decode as an LDAP
461 * modification. It must not be {@code null}.
462 *
463 * @return The decoded LDAP modification.
464 *
465 * @throws LDAPException If a problem occurs while trying to decode the
466 * provided ASN.1 sequence as an LDAP modification.
467 */
468 public static Modification decode(final ASN1Sequence modificationSequence)
469 throws LDAPException
470 {
471 ensureNotNull(modificationSequence);
472
473 final ASN1Element[] modificationElements = modificationSequence.elements();
474 if (modificationElements.length != 2)
475 {
476 throw new LDAPException(ResultCode.DECODING_ERROR,
477 ERR_MOD_DECODE_INVALID_ELEMENT_COUNT.get(
478 modificationElements.length));
479 }
480
481 final int modType;
482 try
483 {
484 final ASN1Enumerated typeEnumerated =
485 ASN1Enumerated.decodeAsEnumerated(modificationElements[0]);
486 modType = typeEnumerated.intValue();
487 }
488 catch (final ASN1Exception ae)
489 {
490 debugException(ae);
491 throw new LDAPException(ResultCode.DECODING_ERROR,
492 ERR_MOD_DECODE_CANNOT_PARSE_MOD_TYPE.get(getExceptionMessage(ae)),
493 ae);
494 }
495
496 final ASN1Sequence attrSequence;
497 try
498 {
499 attrSequence = ASN1Sequence.decodeAsSequence(modificationElements[1]);
500 }
501 catch (final ASN1Exception ae)
502 {
503 debugException(ae);
504 throw new LDAPException(ResultCode.DECODING_ERROR,
505 ERR_MOD_DECODE_CANNOT_PARSE_ATTR.get(getExceptionMessage(ae)), ae);
506 }
507
508 final ASN1Element[] attrElements = attrSequence.elements();
509 if (attrElements.length != 2)
510 {
511 throw new LDAPException(ResultCode.DECODING_ERROR,
512 ERR_MOD_DECODE_INVALID_ATTR_ELEMENT_COUNT.get(
513 attrElements.length));
514 }
515
516 final String attrName =
517 ASN1OctetString.decodeAsOctetString(attrElements[0]).stringValue();
518
519 final ASN1Set valueSet;
520 try
521 {
522 valueSet = ASN1Set.decodeAsSet(attrElements[1]);
523 }
524 catch (final ASN1Exception ae)
525 {
526 debugException(ae);
527 throw new LDAPException(ResultCode.DECODING_ERROR,
528 ERR_MOD_DECODE_CANNOT_PARSE_ATTR_VALUE_SET.get(
529 getExceptionMessage(ae)), ae);
530 }
531
532 final ASN1Element[] valueElements = valueSet.elements();
533 final ASN1OctetString[] values = new ASN1OctetString[valueElements.length];
534 for (int i=0; i < values.length; i++)
535 {
536 values[i] = ASN1OctetString.decodeAsOctetString(valueElements[i]);
537 }
538
539 return new Modification(ModificationType.valueOf(modType), attrName,
540 values);
541 }
542
543
544
545 /**
546 * Calculates a hash code for this LDAP modification.
547 *
548 * @return The generated hash code for this LDAP modification.
549 */
550 @Override()
551 public int hashCode()
552 {
553 int hashCode = modificationType.intValue() +
554 toLowerCase(attributeName).hashCode();
555
556 for (final ASN1OctetString value : values)
557 {
558 hashCode += value.hashCode();
559 }
560
561 return hashCode;
562 }
563
564
565
566 /**
567 * Indicates whether the provided object is equal to this LDAP modification.
568 * The provided object will only be considered equal if it is an LDAP
569 * modification with the same modification type, attribute name, and set of
570 * values as this LDAP modification.
571 *
572 * @param o The object for which to make the determination.
573 *
574 * @return {@code true} if the provided object is equal to this modification,
575 * or {@code false} if not.
576 */
577 @Override()
578 public boolean equals(final Object o)
579 {
580 if (o == null)
581 {
582 return false;
583 }
584
585 if (o == this)
586 {
587 return true;
588 }
589
590 if (! (o instanceof Modification))
591 {
592 return false;
593 }
594
595 final Modification mod = (Modification) o;
596 if (modificationType != mod.modificationType)
597 {
598 return false;
599 }
600
601 if (! attributeName.equalsIgnoreCase(mod.attributeName))
602 {
603 return false;
604 }
605
606 if (values.length != mod.values.length)
607 {
608 return false;
609 }
610
611 // Look at the values using a byte-for-byte matching.
612 for (final ASN1OctetString value : values)
613 {
614 boolean found = false;
615 for (int j = 0; j < mod.values.length; j++)
616 {
617 if (value.equalsIgnoreType(mod.values[j]))
618 {
619 found = true;
620 break;
621 }
622 }
623
624 if (!found)
625 {
626 return false;
627 }
628 }
629
630 // If we've gotten here, then we can consider the object equal to this LDAP
631 // modification.
632 return true;
633 }
634
635
636
637 /**
638 * Retrieves a string representation of this LDAP modification.
639 *
640 * @return A string representation of this LDAP modification.
641 */
642 @Override()
643 public String toString()
644 {
645 final StringBuilder buffer = new StringBuilder();
646 toString(buffer);
647 return buffer.toString();
648 }
649
650
651
652 /**
653 * Appends a string representation of this LDAP modification to the provided
654 * buffer.
655 *
656 * @param buffer The buffer to which to append the string representation of
657 * this LDAP modification.
658 */
659 public void toString(final StringBuilder buffer)
660 {
661 buffer.append("LDAPModification(type=");
662
663 switch (modificationType.intValue())
664 {
665 case 0:
666 buffer.append("add");
667 break;
668 case 1:
669 buffer.append("delete");
670 break;
671 case 2:
672 buffer.append("replace");
673 break;
674 case 3:
675 buffer.append("increment");
676 break;
677 default:
678 buffer.append(modificationType);
679 break;
680 }
681
682 buffer.append(", attr=");
683 buffer.append(attributeName);
684
685 if (values.length == 0)
686 {
687 buffer.append(", values={");
688 }
689 else if (needsBase64Encoding())
690 {
691 buffer.append(", base64Values={'");
692
693 for (int i=0; i < values.length; i++)
694 {
695 if (i > 0)
696 {
697 buffer.append("', '");
698 }
699
700 buffer.append(Base64.encode(values[i].getValue()));
701 }
702
703 buffer.append('\'');
704 }
705 else
706 {
707 buffer.append(", values={'");
708
709 for (int i=0; i < values.length; i++)
710 {
711 if (i > 0)
712 {
713 buffer.append("', '");
714 }
715
716 buffer.append(values[i].stringValue());
717 }
718
719 buffer.append('\'');
720 }
721
722 buffer.append("})");
723 }
724
725
726
727 /**
728 * Indicates whether this modification needs to be base64-encoded when
729 * represented as LDIF.
730 *
731 * @return {@code true} if this modification needs to be base64-encoded when
732 * represented as LDIF, or {@code false} if not.
733 */
734 private boolean needsBase64Encoding()
735 {
736 for (final ASN1OctetString s : values)
737 {
738 if (Attribute.needsBase64Encoding(s.getValue()))
739 {
740 return true;
741 }
742 }
743
744 return false;
745 }
746
747
748
749 /**
750 * Appends a number of lines comprising the Java source code that can be used
751 * to recreate this modification to the given list. Note that unless a first
752 * line prefix and/or last line suffix are provided, this will just include
753 * the code for the constructor, starting with "new Modification(" and ending
754 * with the closing parenthesis for that constructor.
755 *
756 * @param lineList The list to which the source code lines should be
757 * added.
758 * @param indentSpaces The number of spaces that should be used to indent
759 * the generated code. It must not be negative.
760 * @param firstLinePrefix An optional string that should precede
761 * "new Modification(" on the first line of the
762 * generated code (e.g., it could be used for an
763 * attribute assignment, like "Modification m = ").
764 * It may be {@code null} or empty if there should be
765 * no first line prefix.
766 * @param lastLineSuffix An optional suffix that should follow the closing
767 * parenthesis of the constructor (e.g., it could be
768 * a semicolon to represent the end of a Java
769 * statement or a comma to separate it from another
770 * element in an array). It may be {@code null} or
771 * empty if there should be no last line suffix.
772 */
773 public void toCode(final List<String> lineList, final int indentSpaces,
774 final String firstLinePrefix, final String lastLineSuffix)
775 {
776 // Generate a string with the appropriate indent.
777 final StringBuilder buffer = new StringBuilder();
778 for (int i=0; i < indentSpaces; i++)
779 {
780 buffer.append(' ');
781 }
782 final String indent = buffer.toString();
783
784
785 // Start the constructor.
786 buffer.setLength(0);
787 buffer.append(indent);
788 if (firstLinePrefix != null)
789 {
790 buffer.append(firstLinePrefix);
791 }
792 buffer.append("new Modification(");
793 lineList.add(buffer.toString());
794
795 // There will always be a modification type.
796 buffer.setLength(0);
797 buffer.append(indent);
798 buffer.append(" \"ModificationType.");
799 buffer.append(modificationType.getName());
800 buffer.append(',');
801 lineList.add(buffer.toString());
802
803
804 // There will always be an attribute name.
805 buffer.setLength(0);
806 buffer.append(indent);
807 buffer.append(" \"");
808 buffer.append(attributeName);
809 buffer.append('"');
810
811
812 // If the attribute has any values, then include each on its own line.
813 // If possible, represent the values as strings, but fall back to using
814 // byte arrays if necessary. But if this is something we might consider a
815 // sensitive attribute (like a password), then use fake values in the form
816 // "---redacted-value-N---" to indicate that the actual value has been
817 // hidden but to still show the correct number of values.
818 if (values.length > 0)
819 {
820 boolean allPrintable = true;
821
822 final ASN1OctetString[] attrValues;
823 if (isSensitiveToCodeAttribute(attributeName))
824 {
825 attrValues = new ASN1OctetString[values.length];
826 for (int i=0; i < values.length; i++)
827 {
828 attrValues[i] =
829 new ASN1OctetString("---redacted-value-" + (i+1) + "---");
830 }
831 }
832 else
833 {
834 attrValues = values;
835 for (final ASN1OctetString v : values)
836 {
837 if (! isPrintableString(v.getValue()))
838 {
839 allPrintable = false;
840 break;
841 }
842 }
843 }
844
845 for (final ASN1OctetString v : attrValues)
846 {
847 buffer.append(',');
848 lineList.add(buffer.toString());
849
850 buffer.setLength(0);
851 buffer.append(indent);
852 buffer.append(" ");
853 if (allPrintable)
854 {
855 buffer.append('"');
856 buffer.append(v.stringValue());
857 buffer.append('"');
858 }
859 else
860 {
861 byteArrayToCode(v.getValue(), buffer);
862 }
863 }
864 }
865
866
867 // Append the closing parenthesis and any last line suffix.
868 buffer.append(')');
869 if (lastLineSuffix != null)
870 {
871 buffer.append(lastLineSuffix);
872 }
873 lineList.add(buffer.toString());
874 }
875 }