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