jdk/src/share/classes/javax/management/ImmutableDescriptor.java
changeset 2 90ce3da70b43
child 900 55c9c5a88bde
equal deleted inserted replaced
0:fd16c54261b3 2:90ce3da70b43
       
     1 /*
       
     2  * Copyright 2004-2006 Sun Microsystems, Inc.  All Rights Reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Sun designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Sun in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
       
    22  * CA 95054 USA or visit www.sun.com if you need additional information or
       
    23  * have any questions.
       
    24  */
       
    25 
       
    26 package javax.management;
       
    27 
       
    28 import java.io.InvalidObjectException;
       
    29 import java.lang.reflect.Array;
       
    30 import java.util.Arrays;
       
    31 import java.util.Comparator;
       
    32 import java.util.Map;
       
    33 import java.util.SortedMap;
       
    34 import java.util.TreeMap;
       
    35 
       
    36 /**
       
    37  * An immutable descriptor.
       
    38  * @since 1.6
       
    39  */
       
    40 public class ImmutableDescriptor implements Descriptor {
       
    41     private static final long serialVersionUID = 8853308591080540165L;
       
    42 
       
    43     /**
       
    44      * The names of the fields in this ImmutableDescriptor with their
       
    45      * original case.  The names must be in alphabetical order as determined
       
    46      * by {@link String#CASE_INSENSITIVE_ORDER}.
       
    47      */
       
    48     private final String[] names;
       
    49     /**
       
    50      * The values of the fields in this ImmutableDescriptor.  The
       
    51      * elements in this array match the corresponding elements in the
       
    52      * {@code names} array.
       
    53      */
       
    54     private final Object[] values;
       
    55 
       
    56     private transient int hashCode = -1;
       
    57 
       
    58     /**
       
    59      * An empty descriptor.
       
    60      */
       
    61     public static final ImmutableDescriptor EMPTY_DESCRIPTOR =
       
    62             new ImmutableDescriptor();
       
    63 
       
    64     /**
       
    65      * Construct a descriptor containing the given fields and values.
       
    66      *
       
    67      * @throws IllegalArgumentException if either array is null, or
       
    68      * if the arrays have different sizes, or
       
    69      * if a field name is null or empty, or if the same field name
       
    70      * appears more than once.
       
    71      */
       
    72     public ImmutableDescriptor(String[] fieldNames, Object[] fieldValues) {
       
    73         this(makeMap(fieldNames, fieldValues));
       
    74     }
       
    75 
       
    76     /**
       
    77      * Construct a descriptor containing the given fields.  Each String
       
    78      * must be of the form {@code fieldName=fieldValue}.  The field name
       
    79      * ends at the first {@code =} character; for example if the String
       
    80      * is {@code a=b=c} then the field name is {@code a} and its value
       
    81      * is {@code b=c}.
       
    82      *
       
    83      * @throws IllegalArgumentException if the parameter is null, or
       
    84      * if a field name is empty, or if the same field name appears
       
    85      * more than once, or if one of the strings does not contain
       
    86      * an {@code =} character.
       
    87      */
       
    88     public ImmutableDescriptor(String... fields) {
       
    89         this(makeMap(fields));
       
    90     }
       
    91 
       
    92     /**
       
    93      * <p>Construct a descriptor where the names and values of the fields
       
    94      * are the keys and values of the given Map.</p>
       
    95      *
       
    96      * @throws IllegalArgumentException if the parameter is null, or
       
    97      * if a field name is null or empty, or if the same field name appears
       
    98      * more than once (which can happen because field names are not case
       
    99      * sensitive).
       
   100      */
       
   101     public ImmutableDescriptor(Map<String, ?> fields) {
       
   102         if (fields == null)
       
   103             throw new IllegalArgumentException("Null Map");
       
   104         SortedMap<String, Object> map =
       
   105                 new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
       
   106         for (Map.Entry<String, ?> entry : fields.entrySet()) {
       
   107             String name = entry.getKey();
       
   108             if (name == null || name.equals(""))
       
   109                 throw new IllegalArgumentException("Empty or null field name");
       
   110             if (map.containsKey(name))
       
   111                 throw new IllegalArgumentException("Duplicate name: " + name);
       
   112             map.put(name, entry.getValue());
       
   113         }
       
   114         int size = map.size();
       
   115         this.names = map.keySet().toArray(new String[size]);
       
   116         this.values = map.values().toArray(new Object[size]);
       
   117     }
       
   118 
       
   119     /**
       
   120      * This method can replace a deserialized instance of this
       
   121      * class with another instance.  For example, it might replace
       
   122      * a deserialized empty ImmutableDescriptor with
       
   123      * {@link #EMPTY_DESCRIPTOR}.
       
   124      *
       
   125      * @return the replacement object, which may be {@code this}.
       
   126      *
       
   127      * @throws InvalidObjectException if the read object has invalid fields.
       
   128      */
       
   129     private Object readResolve() throws InvalidObjectException {
       
   130         if (names.length == 0 && getClass() == ImmutableDescriptor.class)
       
   131             return EMPTY_DESCRIPTOR;
       
   132 
       
   133         boolean bad = false;
       
   134         if (names == null || values == null || names.length != values.length)
       
   135             bad = true;
       
   136         if (!bad) {
       
   137             final Comparator<String> compare = String.CASE_INSENSITIVE_ORDER;
       
   138             String lastName = ""; // also catches illegal null name
       
   139             for (int i = 0; i < names.length; i++) {
       
   140                 if (names[i] == null ||
       
   141                         compare.compare(lastName, names[i]) >= 0) {
       
   142                     bad = true;
       
   143                     break;
       
   144                 }
       
   145                 lastName = names[i];
       
   146             }
       
   147         }
       
   148         if (bad)
       
   149             throw new InvalidObjectException("Bad names or values");
       
   150 
       
   151         return this;
       
   152     }
       
   153 
       
   154     private static SortedMap<String, ?> makeMap(String[] fieldNames,
       
   155                                                 Object[] fieldValues) {
       
   156         if (fieldNames == null || fieldValues == null)
       
   157             throw new IllegalArgumentException("Null array parameter");
       
   158         if (fieldNames.length != fieldValues.length)
       
   159             throw new IllegalArgumentException("Different size arrays");
       
   160         SortedMap<String, Object> map =
       
   161                 new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
       
   162         for (int i = 0; i < fieldNames.length; i++) {
       
   163             String name = fieldNames[i];
       
   164             if (name == null || name.equals(""))
       
   165                 throw new IllegalArgumentException("Empty or null field name");
       
   166             Object old = map.put(name, fieldValues[i]);
       
   167             if (old != null) {
       
   168                 throw new IllegalArgumentException("Duplicate field name: " +
       
   169                                                    name);
       
   170             }
       
   171         }
       
   172         return map;
       
   173     }
       
   174 
       
   175     private static SortedMap<String, ?> makeMap(String[] fields) {
       
   176         if (fields == null)
       
   177             throw new IllegalArgumentException("Null fields parameter");
       
   178         String[] fieldNames = new String[fields.length];
       
   179         String[] fieldValues = new String[fields.length];
       
   180         for (int i = 0; i < fields.length; i++) {
       
   181             String field = fields[i];
       
   182             int eq = field.indexOf('=');
       
   183             if (eq < 0) {
       
   184                 throw new IllegalArgumentException("Missing = character: " +
       
   185                                                    field);
       
   186             }
       
   187             fieldNames[i] = field.substring(0, eq);
       
   188             // makeMap will catch the case where the name is empty
       
   189             fieldValues[i] = field.substring(eq + 1);
       
   190         }
       
   191         return makeMap(fieldNames, fieldValues);
       
   192     }
       
   193 
       
   194     /**
       
   195      * <p>Return an {@code ImmutableDescriptor} whose contents are the union of
       
   196      * the given descriptors.  Every field name that appears in any of
       
   197      * the descriptors will appear in the result with the
       
   198      * value that it has when the method is called.  Subsequent changes
       
   199      * to any of the descriptors do not affect the ImmutableDescriptor
       
   200      * returned here.</p>
       
   201      *
       
   202      * <p>In the simplest case, there is only one descriptor and the
       
   203      * returned {@code ImmutableDescriptor} is a copy of its fields at the
       
   204      * time this method is called:</p>
       
   205      *
       
   206      * <pre>
       
   207      * Descriptor d = something();
       
   208      * ImmutableDescriptor copy = ImmutableDescriptor.union(d);
       
   209      * </pre>
       
   210      *
       
   211      * @param descriptors the descriptors to be combined.  Any of the
       
   212      * descriptors can be null, in which case it is skipped.
       
   213      *
       
   214      * @return an {@code ImmutableDescriptor} that is the union of the given
       
   215      * descriptors.  The returned object may be identical to one of the
       
   216      * input descriptors if it is an ImmutableDescriptor that contains all of
       
   217      * the required fields.
       
   218      *
       
   219      * @throws IllegalArgumentException if two Descriptors contain the
       
   220      * same field name with different associated values.  Primitive array
       
   221      * values are considered the same if they are of the same type with
       
   222      * the same elements.  Object array values are considered the same if
       
   223      * {@link Arrays#deepEquals(Object[],Object[])} returns true.
       
   224      */
       
   225     public static ImmutableDescriptor union(Descriptor... descriptors) {
       
   226         // Optimize the case where exactly one Descriptor is non-Empty
       
   227         // and it is immutable - we can just return it.
       
   228         int index = findNonEmpty(descriptors, 0);
       
   229         if (index < 0)
       
   230             return EMPTY_DESCRIPTOR;
       
   231         if (descriptors[index] instanceof ImmutableDescriptor
       
   232                 && findNonEmpty(descriptors, index + 1) < 0)
       
   233             return (ImmutableDescriptor) descriptors[index];
       
   234 
       
   235         Map<String, Object> map =
       
   236             new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
       
   237         ImmutableDescriptor biggestImmutable = EMPTY_DESCRIPTOR;
       
   238         for (Descriptor d : descriptors) {
       
   239             if (d != null) {
       
   240                 String[] names;
       
   241                 if (d instanceof ImmutableDescriptor) {
       
   242                     ImmutableDescriptor id = (ImmutableDescriptor) d;
       
   243                     names = id.names;
       
   244                     if (id.getClass() == ImmutableDescriptor.class
       
   245                             && names.length > biggestImmutable.names.length)
       
   246                         biggestImmutable = id;
       
   247                 } else
       
   248                     names = d.getFieldNames();
       
   249                 for (String n : names) {
       
   250                     Object v = d.getFieldValue(n);
       
   251                     Object old = map.put(n, v);
       
   252                     if (old != null) {
       
   253                         boolean equal;
       
   254                         if (old.getClass().isArray()) {
       
   255                             equal = Arrays.deepEquals(new Object[] {old},
       
   256                                                       new Object[] {v});
       
   257                         } else
       
   258                             equal = old.equals(v);
       
   259                         if (!equal) {
       
   260                             final String msg =
       
   261                                 "Inconsistent values for descriptor field " +
       
   262                                 n + ": " + old + " :: " + v;
       
   263                             throw new IllegalArgumentException(msg);
       
   264                         }
       
   265                     }
       
   266                 }
       
   267             }
       
   268         }
       
   269         if (biggestImmutable.names.length == map.size())
       
   270             return biggestImmutable;
       
   271         return new ImmutableDescriptor(map);
       
   272     }
       
   273 
       
   274     private static boolean isEmpty(Descriptor d) {
       
   275         if (d == null)
       
   276             return true;
       
   277         else if (d instanceof ImmutableDescriptor)
       
   278             return ((ImmutableDescriptor) d).names.length == 0;
       
   279         else
       
   280             return (d.getFieldNames().length == 0);
       
   281     }
       
   282 
       
   283     private static int findNonEmpty(Descriptor[] ds, int start) {
       
   284         for (int i = start; i < ds.length; i++) {
       
   285             if (!isEmpty(ds[i]))
       
   286                 return i;
       
   287         }
       
   288         return -1;
       
   289     }
       
   290 
       
   291     private int fieldIndex(String name) {
       
   292         return Arrays.binarySearch(names, name, String.CASE_INSENSITIVE_ORDER);
       
   293     }
       
   294 
       
   295     public final Object getFieldValue(String fieldName) {
       
   296         checkIllegalFieldName(fieldName);
       
   297         int i = fieldIndex(fieldName);
       
   298         if (i < 0)
       
   299             return null;
       
   300         Object v = values[i];
       
   301         if (v == null || !v.getClass().isArray())
       
   302             return v;
       
   303         if (v instanceof Object[])
       
   304             return ((Object[]) v).clone();
       
   305         // clone the primitive array, could use an 8-way if/else here
       
   306         int len = Array.getLength(v);
       
   307         Object a = Array.newInstance(v.getClass().getComponentType(), len);
       
   308         System.arraycopy(v, 0, a, 0, len);
       
   309         return a;
       
   310     }
       
   311 
       
   312     public final String[] getFields() {
       
   313         String[] result = new String[names.length];
       
   314         for (int i = 0; i < result.length; i++) {
       
   315             Object value = values[i];
       
   316             if (value == null)
       
   317                 value = "";
       
   318             else if (!(value instanceof String))
       
   319                 value = "(" + value + ")";
       
   320             result[i] = names[i] + "=" + value;
       
   321         }
       
   322         return result;
       
   323     }
       
   324 
       
   325     public final Object[] getFieldValues(String... fieldNames) {
       
   326         if (fieldNames == null)
       
   327             return values.clone();
       
   328         Object[] result = new Object[fieldNames.length];
       
   329         for (int i = 0; i < fieldNames.length; i++) {
       
   330             String name = fieldNames[i];
       
   331             if (name != null && !name.equals(""))
       
   332                 result[i] = getFieldValue(name);
       
   333         }
       
   334         return result;
       
   335     }
       
   336 
       
   337     public final String[] getFieldNames() {
       
   338         return names.clone();
       
   339     }
       
   340 
       
   341     /**
       
   342      * Compares this descriptor to the given object.  The objects are equal if
       
   343      * the given object is also a Descriptor, and if the two Descriptors have
       
   344      * the same field names (possibly differing in case) and the same
       
   345      * associated values.  The respective values for a field in the two
       
   346      * Descriptors are equal if the following conditions hold:</p>
       
   347      *
       
   348      * <ul>
       
   349      * <li>If one value is null then the other must be too.</li>
       
   350      * <li>If one value is a primitive array then the other must be a primitive
       
   351      * array of the same type with the same elements.</li>
       
   352      * <li>If one value is an object array then the other must be too and
       
   353      * {@link Arrays#deepEquals(Object[],Object[])} must return true.</li>
       
   354      * <li>Otherwise {@link Object#equals(Object)} must return true.</li>
       
   355      * </ul>
       
   356      *
       
   357      * @param o the object to compare with.
       
   358      *
       
   359      * @return {@code true} if the objects are the same; {@code false}
       
   360      * otherwise.
       
   361      *
       
   362      */
       
   363     // Note: this Javadoc is copied from javax.management.Descriptor
       
   364     //       due to 6369229.
       
   365     public boolean equals(Object o) {
       
   366         if (o == this)
       
   367             return true;
       
   368         if (!(o instanceof Descriptor))
       
   369             return false;
       
   370         String[] onames;
       
   371         if (o instanceof ImmutableDescriptor) {
       
   372             onames = ((ImmutableDescriptor) o).names;
       
   373         } else {
       
   374             onames = ((Descriptor) o).getFieldNames();
       
   375             Arrays.sort(onames, String.CASE_INSENSITIVE_ORDER);
       
   376         }
       
   377         if (names.length != onames.length)
       
   378             return false;
       
   379         for (int i = 0; i < names.length; i++) {
       
   380             if (!names[i].equalsIgnoreCase(onames[i]))
       
   381                 return false;
       
   382         }
       
   383         Object[] ovalues;
       
   384         if (o instanceof ImmutableDescriptor)
       
   385             ovalues = ((ImmutableDescriptor) o).values;
       
   386         else
       
   387             ovalues = ((Descriptor) o).getFieldValues(onames);
       
   388         return Arrays.deepEquals(values, ovalues);
       
   389     }
       
   390 
       
   391     /**
       
   392      * <p>Returns the hash code value for this descriptor.  The hash
       
   393      * code is computed as the sum of the hash codes for each field in
       
   394      * the descriptor.  The hash code of a field with name {@code n}
       
   395      * and value {@code v} is {@code n.toLowerCase().hashCode() ^ h}.
       
   396      * Here {@code h} is the hash code of {@code v}, computed as
       
   397      * follows:</p>
       
   398      *
       
   399      * <ul>
       
   400      * <li>If {@code v} is null then {@code h} is 0.</li>
       
   401      * <li>If {@code v} is a primitive array then {@code h} is computed using
       
   402      * the appropriate overloading of {@code java.util.Arrays.hashCode}.</li>
       
   403      * <li>If {@code v} is an object array then {@code h} is computed using
       
   404      * {@link Arrays#deepHashCode(Object[])}.</li>
       
   405      * <li>Otherwise {@code h} is {@code v.hashCode()}.</li>
       
   406      * </ul>
       
   407      *
       
   408      * @return A hash code value for this object.
       
   409      *
       
   410      */
       
   411     // Note: this Javadoc is copied from javax.management.Descriptor
       
   412     //       due to 6369229.
       
   413     public int hashCode() {
       
   414         if (hashCode == -1) {
       
   415             int hash = 0;
       
   416             for (int i = 0; i < names.length; i++) {
       
   417                 Object v = values[i];
       
   418                 int h;
       
   419                 if (v == null)
       
   420                     h = 0;
       
   421                 else if (v instanceof Object[])
       
   422                     h = Arrays.deepHashCode((Object[]) v);
       
   423                 else if (v.getClass().isArray()) {
       
   424                     h = Arrays.deepHashCode(new Object[] {v}) - 31;
       
   425                     // hashcode of a list containing just v is
       
   426                     // v.hashCode() + 31, see List.hashCode()
       
   427                 } else
       
   428                     h = v.hashCode();
       
   429                 hash += names[i].toLowerCase().hashCode() ^ h;
       
   430             }
       
   431             hashCode = hash;
       
   432         }
       
   433         return hashCode;
       
   434     }
       
   435 
       
   436     public String toString() {
       
   437         StringBuilder sb = new StringBuilder("{");
       
   438         for (int i = 0; i < names.length; i++) {
       
   439             if (i > 0)
       
   440                 sb.append(", ");
       
   441             sb.append(names[i]).append("=");
       
   442             Object v = values[i];
       
   443             if (v != null && v.getClass().isArray()) {
       
   444                 String s = Arrays.deepToString(new Object[] {v});
       
   445                 s = s.substring(1, s.length() - 1); // remove [...]
       
   446                 v = s;
       
   447             }
       
   448             sb.append(String.valueOf(v));
       
   449         }
       
   450         return sb.append("}").toString();
       
   451     }
       
   452 
       
   453     /**
       
   454      * Returns true if all of the fields have legal values given their
       
   455      * names.  This method always returns true, but a subclass can
       
   456      * override it to return false when appropriate.
       
   457      *
       
   458      * @return true if the values are legal.
       
   459      *
       
   460      * @exception RuntimeOperationsException if the validity checking fails.
       
   461      * The method returns false if the descriptor is not valid, but throws
       
   462      * this exception if the attempt to determine validity fails.
       
   463      */
       
   464     public boolean isValid() {
       
   465         return true;
       
   466     }
       
   467 
       
   468     /**
       
   469      * <p>Returns a descriptor which is equal to this descriptor.
       
   470      * Changes to the returned descriptor will have no effect on this
       
   471      * descriptor, and vice versa.</p>
       
   472      *
       
   473      * <p>This method returns the object on which it is called.
       
   474      * A subclass can override it
       
   475      * to return another object provided the contract is respected.
       
   476      *
       
   477      * @exception RuntimeOperationsException for illegal value for field Names
       
   478      * or field Values.
       
   479      * If the descriptor construction fails for any reason, this exception will
       
   480      * be thrown.
       
   481      */
       
   482     public Descriptor clone() {
       
   483         return this;
       
   484     }
       
   485 
       
   486     /**
       
   487      * This operation is unsupported since this class is immutable.  If
       
   488      * this call would change a mutable descriptor with the same contents,
       
   489      * then a {@link RuntimeOperationsException} wrapping an
       
   490      * {@link UnsupportedOperationException} is thrown.  Otherwise,
       
   491      * the behavior is the same as it would be for a mutable descriptor:
       
   492      * either an exception is thrown because of illegal parameters, or
       
   493      * there is no effect.
       
   494      */
       
   495     public final void setFields(String[] fieldNames, Object[] fieldValues)
       
   496         throws RuntimeOperationsException {
       
   497         if (fieldNames == null || fieldValues == null)
       
   498             illegal("Null argument");
       
   499         if (fieldNames.length != fieldValues.length)
       
   500             illegal("Different array sizes");
       
   501         for (int i = 0; i < fieldNames.length; i++)
       
   502             checkIllegalFieldName(fieldNames[i]);
       
   503         for (int i = 0; i < fieldNames.length; i++)
       
   504             setField(fieldNames[i], fieldValues[i]);
       
   505     }
       
   506 
       
   507     /**
       
   508      * This operation is unsupported since this class is immutable.  If
       
   509      * this call would change a mutable descriptor with the same contents,
       
   510      * then a {@link RuntimeOperationsException} wrapping an
       
   511      * {@link UnsupportedOperationException} is thrown.  Otherwise,
       
   512      * the behavior is the same as it would be for a mutable descriptor:
       
   513      * either an exception is thrown because of illegal parameters, or
       
   514      * there is no effect.
       
   515      */
       
   516     public final void setField(String fieldName, Object fieldValue)
       
   517         throws RuntimeOperationsException {
       
   518         checkIllegalFieldName(fieldName);
       
   519         int i = fieldIndex(fieldName);
       
   520         if (i < 0)
       
   521             unsupported();
       
   522         Object value = values[i];
       
   523         if ((value == null) ?
       
   524                 (fieldValue != null) :
       
   525                 !value.equals(fieldValue))
       
   526             unsupported();
       
   527     }
       
   528 
       
   529     /**
       
   530      * Removes a field from the descriptor.
       
   531      *
       
   532      * @param fieldName String name of the field to be removed.
       
   533      * If the field name is illegal or the field is not found,
       
   534      * no exception is thrown.
       
   535      *
       
   536      * @exception RuntimeOperationsException if a field of the given name
       
   537      * exists and the descriptor is immutable.  The wrapped exception will
       
   538      * be an {@link UnsupportedOperationException}.
       
   539      */
       
   540     public final void removeField(String fieldName) {
       
   541         if (fieldName != null && fieldIndex(fieldName) >= 0)
       
   542             unsupported();
       
   543     }
       
   544 
       
   545     static Descriptor nonNullDescriptor(Descriptor d) {
       
   546         if (d == null)
       
   547             return EMPTY_DESCRIPTOR;
       
   548         else
       
   549             return d;
       
   550     }
       
   551 
       
   552     private static void checkIllegalFieldName(String name) {
       
   553         if (name == null || name.equals(""))
       
   554             illegal("Null or empty field name");
       
   555     }
       
   556 
       
   557     private static void unsupported() {
       
   558         UnsupportedOperationException uoe =
       
   559             new UnsupportedOperationException("Descriptor is read-only");
       
   560         throw new RuntimeOperationsException(uoe);
       
   561     }
       
   562 
       
   563     private static void illegal(String message) {
       
   564         IllegalArgumentException iae = new IllegalArgumentException(message);
       
   565         throw new RuntimeOperationsException(iae);
       
   566     }
       
   567 }