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