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