6602310: Extensions to Query API for JMX 2.0
authoremcmanus
Mon, 03 Mar 2008 10:32:38 +0100
changeset 34 2d042367885f
parent 25 74fe6922716d
child 35 935f747e70d3
child 36 86095028f162
6602310: Extensions to Query API for JMX 2.0 6604768: IN queries require their arguments to be constants Summary: New JMX query language and support for dotted attributes in queries. Reviewed-by: dfuchs
jdk/src/share/classes/com/sun/jmx/mbeanserver/Introspector.java
jdk/src/share/classes/javax/management/AndQueryExp.java
jdk/src/share/classes/javax/management/AttributeValueExp.java
jdk/src/share/classes/javax/management/BetweenQueryExp.java
jdk/src/share/classes/javax/management/BinaryOpValueExp.java
jdk/src/share/classes/javax/management/BinaryRelQueryExp.java
jdk/src/share/classes/javax/management/BooleanValueExp.java
jdk/src/share/classes/javax/management/InQueryExp.java
jdk/src/share/classes/javax/management/MatchQueryExp.java
jdk/src/share/classes/javax/management/NotQueryExp.java
jdk/src/share/classes/javax/management/NumericValueExp.java
jdk/src/share/classes/javax/management/ObjectName.java
jdk/src/share/classes/javax/management/OrQueryExp.java
jdk/src/share/classes/javax/management/QualifiedAttributeValueExp.java
jdk/src/share/classes/javax/management/Query.java
jdk/src/share/classes/javax/management/QueryEval.java
jdk/src/share/classes/javax/management/QueryExp.java
jdk/src/share/classes/javax/management/QueryParser.java
jdk/src/share/classes/javax/management/StringValueExp.java
jdk/src/share/classes/javax/management/ToQueryString.java
jdk/src/share/classes/javax/management/monitor/Monitor.java
jdk/test/javax/management/query/QueryDottedAttrTest.java
jdk/test/javax/management/query/QueryExpStringTest.java
jdk/test/javax/management/query/QueryParseTest.java
--- a/jdk/src/share/classes/com/sun/jmx/mbeanserver/Introspector.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/com/sun/jmx/mbeanserver/Introspector.java	Mon Mar 03 10:32:38 2008 +0100
@@ -43,6 +43,13 @@
 import javax.management.NotCompliantMBeanException;
 
 import com.sun.jmx.mbeanserver.Util;
+import com.sun.jmx.remote.util.EnvHelp;
+import java.beans.BeanInfo;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import javax.management.AttributeNotFoundException;
+import javax.management.openmbean.CompositeData;
 
 /**
  * This class contains the methods for performing all the tests needed to verify
@@ -482,4 +489,33 @@
 
         return null;
     }
+
+    public static Object elementFromComplex(Object complex, String element)
+    throws AttributeNotFoundException {
+        try {
+            if (complex.getClass().isArray() && element.equals("length")) {
+                return Array.getLength(complex);
+            } else if (complex instanceof CompositeData) {
+                return ((CompositeData) complex).get(element);
+            } else {
+                // Java Beans introspection
+                //
+                BeanInfo bi = java.beans.Introspector.getBeanInfo(complex.getClass());
+                PropertyDescriptor[] pds = bi.getPropertyDescriptors();
+                for (PropertyDescriptor pd : pds)
+                    if (pd.getName().equals(element))
+                        return pd.getReadMethod().invoke(complex);
+                throw new AttributeNotFoundException(
+                    "Could not find the getter method for the property " +
+                    element + " using the Java Beans introspector");
+            }
+        } catch (InvocationTargetException e) {
+            throw new IllegalArgumentException(e);
+        } catch (AttributeNotFoundException e) {
+            throw e;
+        } catch (Exception e) {
+            throw EnvHelp.initCause(
+                new AttributeNotFoundException(e.getMessage()), e);
+        }
+    }
 }
--- a/jdk/src/share/classes/javax/management/AndQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/AndQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -104,4 +104,25 @@
      return "(" + exp1 + ") and (" + exp2 + ")";
    }
 
- }
+   @Override
+   String toQueryString() {
+        // Parentheses are only added if needed to disambiguate.
+        return parens(exp1) + " and " + parens(exp2);
+   }
+
+   // Add parens if needed to disambiguate an expression such as
+   // Query.and(Query.or(a, b), c).  We need to return
+   // (a or b) and c
+   // in such a case, because
+   // a or b and c
+   // would mean
+   // a or (b and c)
+   private static String parens(QueryExp exp) {
+       String s = Query.toString(exp);
+       if (exp instanceof OrQueryExp)
+           return "(" + s + ")";
+       else
+           return s;
+   }
+
+}
--- a/jdk/src/share/classes/javax/management/AttributeValueExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/AttributeValueExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -26,12 +26,17 @@
 package javax.management;
 
 
-// RI import
-import javax.management.MBeanServer;
+import com.sun.jmx.mbeanserver.Introspector;
+import java.io.IOException;
+import java.io.ObjectInputStream;
 
 /**
- * Represents attributes used as arguments to relational constraints.
- * An <CODE>AttributeValueExp</CODE> may be used anywhere a <CODE>ValueExp</CODE> is required.
+ * <p>Represents attributes used as arguments to relational constraints.
+ * Instances of this class are usually obtained using {@link Query#attr(String)
+ * Query.attr}.</p>
+ *
+ * <p>An <CODE>AttributeValueExp</CODE> may be used anywhere a
+ * <CODE>ValueExp</CODE> is required.
  *
  * @since 1.5
  */
@@ -46,6 +51,8 @@
      */
     private String attr;
 
+    private transient int dotIndex;
+
     /**
      * An <code>AttributeValueExp</code> with a null attribute.
      * @deprecated An instance created with this constructor cannot be
@@ -64,6 +71,18 @@
      */
     public AttributeValueExp(String attr) {
         this.attr = attr;
+        setDotIndex();
+    }
+
+    private void setDotIndex() {
+        if (attr != null)
+            dotIndex = attr.indexOf('.');
+    }
+
+    private void readObject(ObjectInputStream in)
+    throws ClassNotFoundException, IOException {
+        in.defaultReadObject();
+        setDotIndex();
     }
 
     /**
@@ -76,7 +95,13 @@
     }
 
     /**
-     * Applies the <CODE>AttributeValueExp</CODE> on an MBean.
+     * <p>Applies the <CODE>AttributeValueExp</CODE> on an MBean.
+     * This method calls {@link #getAttribute getAttribute(name)} and wraps
+     * the result as a {@code ValueExp}.  The value returned by
+     * {@code getAttribute} must be a {@code Number}, {@code String},
+     * or {@code Boolean}; otherwise this method throws a
+     * {@code BadAttributeValueExpException}, which will cause
+     * the containing query to be false for this {@code name}.</p>
      *
      * @param name The name of the MBean on which the <CODE>AttributeValueExp</CODE> will be applied.
      *
@@ -88,6 +113,7 @@
      * @exception BadBinaryOpValueExpException
      *
      */
+    @Override
     public ValueExp apply(ObjectName name) throws BadStringOperationException, BadBinaryOpValueExpException,
         BadAttributeValueExpException, InvalidApplicationException {
         Object result = getAttribute(name);
@@ -106,8 +132,9 @@
     /**
      * Returns the string representing its value.
      */
+    @Override
     public String toString()  {
-        return attr;
+        return QueryParser.quoteId(attr);
     }
 
 
@@ -115,18 +142,38 @@
      * Sets the MBean server on which the query is to be performed.
      *
      * @param s The MBean server on which the query is to be performed.
+     *
+     * @deprecated This method has no effect.  The MBean Server used to
+     * obtain an attribute value is {@link QueryEval#getMBeanServer()}.
      */
     /* There is no need for this method, because if a query is being
        evaluted an AttributeValueExp can only appear inside a QueryExp,
        and that QueryExp will itself have done setMBeanServer.  */
+    @Deprecated
+    @Override
     public void setMBeanServer(MBeanServer s)  {
     }
 
 
     /**
-     * Return the value of the given attribute in the named MBean.
+     * <p>Return the value of the given attribute in the named MBean.
      * If the attempt to access the attribute generates an exception,
-     * return null.
+     * return null.</p>
+     *
+     * <p>Let <em>n</em> be the {@linkplain #getAttributeName attribute
+     * name}. Then this method proceeds as follows. First it calls
+     * {@link MBeanServer#getAttribute getAttribute(name, <em>n</em>)}. If that
+     * generates an {@link AttributeNotFoundException}, and if <em>n</em>
+     * contains at least one dot ({@code .}), then the method calls {@code
+     * getAttribute(name, }<em>n</em>{@code .substring(0, }<em>n</em>{@code
+     * .indexOf('.')))}; in other words it calls {@code getAttribute}
+     * with the substring of <em>n</em> before the first dot. Then it
+     * extracts a component from the retrieved value, as described in the <a
+     * href="monitor/package-summary.html#complex">documentation for the {@code
+     * monitor} package</a>.</p>
+     *
+     * <p>The MBean Server used is the one returned by {@link
+     * QueryEval#getMBeanServer()}.</p>
      *
      * @param name the name of the MBean whose attribute is to be returned.
      *
@@ -139,10 +186,34 @@
 
             MBeanServer server = QueryEval.getMBeanServer();
 
+            try {
             return server.getAttribute(name, attr);
+            } catch (AttributeNotFoundException e) {
+                if (dotIndex < 0)
+                    throw e;
+            }
+
+            String toGet = attr.substring(0, dotIndex);
+
+            Object value = server.getAttribute(name, toGet);
+
+            return extractElement(value, attr.substring(dotIndex + 1));
         } catch (Exception re) {
             return null;
         }
     }
 
+    private Object extractElement(Object value, String elementWithDots)
+    throws AttributeNotFoundException {
+        while (true) {
+            int dot = elementWithDots.indexOf('.');
+            String element = (dot < 0) ?
+                elementWithDots : elementWithDots.substring(0, dot);
+            value = Introspector.elementFromComplex(value, element);
+            if (dot < 0)
+                return value;
+            elementWithDots = elementWithDots.substring(dot + 1);
+        }
+    }
+
 }
--- a/jdk/src/share/classes/javax/management/BetweenQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/BetweenQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -109,34 +109,25 @@
         ValueExp val1 = exp1.apply(name);
         ValueExp val2 = exp2.apply(name);
         ValueExp val3 = exp3.apply(name);
-        String sval1;
-        String sval2;
-        String sval3;
-        double dval1;
-        double dval2;
-        double dval3;
-        long lval1;
-        long lval2;
-        long lval3;
         boolean numeric = val1 instanceof NumericValueExp;
 
         if (numeric) {
             if (((NumericValueExp)val1).isLong()) {
-                lval1 = ((NumericValueExp)val1).longValue();
-                lval2 = ((NumericValueExp)val2).longValue();
-                lval3 = ((NumericValueExp)val3).longValue();
+                long lval1 = ((NumericValueExp)val1).longValue();
+                long lval2 = ((NumericValueExp)val2).longValue();
+                long lval3 = ((NumericValueExp)val3).longValue();
                 return lval2 <= lval1 && lval1 <= lval3;
             } else {
-                dval1 = ((NumericValueExp)val1).doubleValue();
-                dval2 = ((NumericValueExp)val2).doubleValue();
-                dval3 = ((NumericValueExp)val3).doubleValue();
+                double dval1 = ((NumericValueExp)val1).doubleValue();
+                double dval2 = ((NumericValueExp)val2).doubleValue();
+                double dval3 = ((NumericValueExp)val3).doubleValue();
                 return dval2 <= dval1 && dval1 <= dval3;
             }
 
         } else {
-            sval1 = ((StringValueExp)val1).toString();
-            sval2 = ((StringValueExp)val2).toString();
-            sval3 = ((StringValueExp)val3).toString();
+            String sval1 = ((StringValueExp)val1).getValue();
+            String sval2 = ((StringValueExp)val2).getValue();
+            String sval3 = ((StringValueExp)val3).getValue();
             return sval2.compareTo(sval1) <= 0 && sval1.compareTo(sval3) <= 0;
         }
     }
@@ -148,4 +139,8 @@
         return "(" + exp1 + ") between (" + exp2 + ") and (" + exp3 + ")";
     }
 
- }
+    @Override
+    String toQueryString() {
+        return exp1 + " between " + exp2 + " and " + exp3;
+    }
+}
--- a/jdk/src/share/classes/javax/management/BinaryOpValueExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/BinaryOpValueExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -167,12 +167,74 @@
      */
     public String toString()  {
         try {
-            return exp1 + " " + opString() + " " + exp2;
+            return parens(exp1, true) + " " + opString() + " " + parens(exp2, false);
         } catch (BadBinaryOpValueExpException ex) {
             return "invalid expression";
         }
     }
 
+    /*
+     * Add parentheses to the given subexpression if necessary to
+     * preserve meaning.  Suppose this BinaryOpValueExp is
+     * Query.times(Query.plus(Query.attr("A"), Query.attr("B")), Query.attr("C")).
+     * Then the original toString() logic would return A + B * C.
+     * We check precedences in order to return (A + B) * C, which is the
+     * meaning of the ValueExp.
+     *
+     * We need to add parentheses if the unparenthesized expression would
+     * be parsed as a different ValueExp from the original.
+     * We cannot omit parentheses even when mathematically
+     * the result would be equivalent, because we do not know whether the
+     * numeric values will be integer or floating-point.  Addition and
+     * multiplication are associative for integers but not always for
+     * floating-point.
+     *
+     * So the rule is that we omit parentheses if the ValueExp
+     * is (A op1 B) op2 C and the precedence of op1 is greater than or
+     * equal to that of op2; or if the ValueExp is A op1 (B op2 C) and
+     * the precedence of op2 is greater than that of op1.  (There are two
+     * precedences: that of * and / is greater than that of + and -.)
+     * The case of (A op1 B) op2 (C op3 D) applies each rule in turn.
+     *
+     * The following examples show the rules in action.  On the left,
+     * the original ValueExp.  On the right, the string representation.
+     *
+     * (A + B) + C     A + B + C
+     * (A * B) + C     A * B + C
+     * (A + B) * C     (A + B) * C
+     * (A * B) * C     A * B * C
+     * A + (B + C)     A + (B + C)
+     * A + (B * C)     A + B * C
+     * A * (B + C)     A * (B + C)
+     * A * (B * C)     A * (B * C)
+     */
+    private String parens(ValueExp subexp, boolean left)
+    throws BadBinaryOpValueExpException {
+        boolean omit;
+        if (subexp instanceof BinaryOpValueExp) {
+            int subop = ((BinaryOpValueExp) subexp).op;
+            if (left)
+                omit = (precedence(subop) >= precedence(op));
+            else
+                omit = (precedence(subop) > precedence(op));
+        } else
+            omit = true;
+
+        if (omit)
+            return subexp.toString();
+        else
+            return "(" + subexp + ")";
+    }
+
+    private int precedence(int xop) throws BadBinaryOpValueExpException {
+        switch (xop) {
+            case Query.PLUS: case Query.MINUS: return 0;
+            case Query.TIMES: case Query.DIV: return 1;
+            default:
+                throw new BadBinaryOpValueExpException(this);
+        }
+    }
+
     private String opString() throws BadBinaryOpValueExpException {
         switch (op) {
         case Query.PLUS:
@@ -188,4 +250,10 @@
         throw new BadBinaryOpValueExpException(this);
     }
 
+    @Deprecated
+    public void setMBeanServer(MBeanServer s) {
+        super.setMBeanServer(s);
  }
+
+
+ }
--- a/jdk/src/share/classes/javax/management/BinaryRelQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/BinaryRelQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -108,20 +108,12 @@
         BadAttributeValueExpException, InvalidApplicationException  {
         Object val1 = exp1.apply(name);
         Object val2 = exp2.apply(name);
-        String sval1;
-        String sval2;
-        double dval1;
-        double dval2;
-        long   lval1;
-        long   lval2;
-        boolean bval1;
-        boolean bval2;
         boolean numeric = val1 instanceof NumericValueExp;
         boolean bool = val1 instanceof BooleanValueExp;
         if (numeric) {
             if (((NumericValueExp)val1).isLong()) {
-                lval1 = ((NumericValueExp)val1).longValue();
-                lval2 = ((NumericValueExp)val2).longValue();
+                long lval1 = ((NumericValueExp)val1).longValue();
+                long lval2 = ((NumericValueExp)val2).longValue();
 
                 switch (relOp) {
                 case Query.GT:
@@ -136,8 +128,8 @@
                     return lval1 == lval2;
                 }
             } else {
-                dval1 = ((NumericValueExp)val1).doubleValue();
-                dval2 = ((NumericValueExp)val2).doubleValue();
+                double dval1 = ((NumericValueExp)val1).doubleValue();
+                double dval2 = ((NumericValueExp)val2).doubleValue();
 
                 switch (relOp) {
                 case Query.GT:
@@ -155,8 +147,8 @@
 
         } else if (bool) {
 
-            bval1 = ((BooleanValueExp)val1).getValue().booleanValue();
-            bval2 = ((BooleanValueExp)val2).getValue().booleanValue();
+            boolean bval1 = ((BooleanValueExp)val1).getValue().booleanValue();
+            boolean bval2 = ((BooleanValueExp)val2).getValue().booleanValue();
 
             switch (relOp) {
             case Query.GT:
@@ -172,8 +164,8 @@
             }
 
         } else {
-            sval1 = ((StringValueExp)val1).getValue();
-            sval2 = ((StringValueExp)val2).getValue();
+            String sval1 = ((StringValueExp)val1).getValue();
+            String sval2 = ((StringValueExp)val2).getValue();
 
             switch (relOp) {
             case Query.GT:
@@ -199,6 +191,11 @@
         return "(" + exp1 + ") " + relOpString() + " (" + exp2 + ")";
     }
 
+    @Override
+    String toQueryString() {
+        return exp1 + " " + relOpString() + " " + exp2;
+    }
+
     private String relOpString() {
         switch (relOp) {
         case Query.GT:
--- a/jdk/src/share/classes/javax/management/BooleanValueExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/BooleanValueExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -84,4 +84,10 @@
         return this;
     }
 
+    @Deprecated
+    public void setMBeanServer(MBeanServer s) {
+        super.setMBeanServer(s);
+    }
+
+
  }
--- a/jdk/src/share/classes/javax/management/InQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/InQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -91,21 +91,23 @@
      * @exception BadAttributeValueExpException
      * @exception InvalidApplicationException
      */
-    public boolean apply(ObjectName name) throws BadStringOperationException, BadBinaryOpValueExpException,
+    public boolean apply(ObjectName name)
+    throws BadStringOperationException, BadBinaryOpValueExpException,
         BadAttributeValueExpException, InvalidApplicationException  {
         if (valueList != null) {
             ValueExp v      = val.apply(name);
             boolean numeric = v instanceof NumericValueExp;
 
-            for (int i = 0; i < valueList.length; i++) {
+            for (ValueExp element : valueList) {
+                element = element.apply(name);
                 if (numeric) {
-                    if (((NumericValueExp)valueList[i]).doubleValue() ==
-                        ((NumericValueExp)v).doubleValue()) {
+                    if (((NumericValueExp) element).doubleValue() ==
+                        ((NumericValueExp) v).doubleValue()) {
                         return true;
                     }
                 } else {
-                    if (((StringValueExp)valueList[i]).getValue().equals(
-                        ((StringValueExp)v).getValue())) {
+                    if (((StringValueExp) element).getValue().equals(
+                        ((StringValueExp) v).getValue())) {
                         return true;
                     }
                 }
--- a/jdk/src/share/classes/javax/management/MatchQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/MatchQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -113,7 +113,32 @@
     }
 
     private static String likeTranslate(String s) {
-        return s.replace('?', '_').replace('*', '%');
+        StringBuilder sb = new StringBuilder();
+        int c;
+        for (int i = 0; i < s.length(); i += Character.charCount(c)) {
+            c = s.codePointAt(i);
+            switch (c) {
+                case '\\':
+                    i += Character.charCount(c);
+                    sb.append('\\');
+                    if (i < s.length()) {
+                        c = s.codePointAt(i);
+                        sb.appendCodePoint(c);
+    }
+                    break;
+                case '*':
+                    sb.append('%'); break;
+                case '?':
+                    sb.append('_'); break;
+                case '%':
+                    sb.append("\\%"); break;
+                case '_':
+                    sb.append("\\_"); break;
+                default:
+                    sb.appendCodePoint(c); break;
+            }
+        }
+        return sb.toString();
     }
 
     /*
--- a/jdk/src/share/classes/javax/management/NotQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/NotQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -86,8 +86,14 @@
     /**
      * Returns the string representing the object.
      */
+    @Override
     public String toString()  {
         return "not (" + exp + ")";
     }
 
+    @Override
+    String toQueryString() {
+        return "not (" + Query.toString(exp) + ")";
+    }
+
  }
--- a/jdk/src/share/classes/javax/management/NumericValueExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/NumericValueExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -151,11 +151,18 @@
      * Returns the string representing the object
      */
     public String toString()  {
+      if (val == null)
+        return "null";
       if (val instanceof Long || val instanceof Integer)
       {
-        return String.valueOf(val.longValue());
+        return Long.toString(val.longValue());
       }
-      return String.valueOf(val.doubleValue());
+      double d = val.doubleValue();
+      if (Double.isInfinite(d))
+          return (d > 0) ? "(1.0 / 0.0)" : "(-1.0 / 0.0)";
+      if (Double.isNaN(d))
+          return "(0.0 / 0.0)";
+      return Double.toString(d);
     }
 
     /**
@@ -244,4 +251,10 @@
         out.defaultWriteObject();
       }
     }
+
+    @Deprecated
+    public void setMBeanServer(MBeanServer s) {
+        super.setMBeanServer(s);
+    }
+
  }
--- a/jdk/src/share/classes/javax/management/ObjectName.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/ObjectName.java	Mon Mar 03 10:32:38 2008 +0100
@@ -222,7 +222,8 @@
  * @since 1.5
  */
 @SuppressWarnings("serial") // don't complain serialVersionUID not constant
-public class ObjectName implements Comparable<ObjectName>, QueryExp {
+public class ObjectName extends ToQueryString
+        implements Comparable<ObjectName>, QueryExp {
 
     /**
      * A structure recording property structure and
@@ -1779,10 +1780,16 @@
      *
      * @return a string representation of this object name.
      */
+    @Override
     public String toString()  {
         return getSerializedNameString();
     }
 
+    @Override
+    String toQueryString() {
+        return "LIKE " + Query.value(toString());
+    }
+
     /**
      * Compares the current object name with another object name.  Two
      * ObjectName instances are equal if and only if their canonical
--- a/jdk/src/share/classes/javax/management/OrQueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/OrQueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -98,9 +98,29 @@
     }
 
     /**
-     * Returns a string representation of this AndQueryExp
+     * Returns a string representation of this OrQueryExp
      */
     public String toString() {
         return "(" + exp1 + ") or (" + exp2 + ")";
     }
+
+    @Override
+    String toQueryString() {
+        return parens(exp1) + " or " + parens(exp2);
+    }
+
+    // Add parentheses to avoid possible confusion.  If we have an expression
+    // such as Query.or(Query.and(a, b), c), then we return
+    // (a and b) or c
+    // rather than just
+    // a and b or c
+    // In fact the precedence rules are such that the parentheses are not
+    // strictly necessary, but omitting them would be confusing.
+    private static String parens(QueryExp exp) {
+        String s = Query.toString(exp);
+        if (exp instanceof AndQueryExp)
+            return "(" + s + ")";
+        else
+            return s;
+    }
 }
--- a/jdk/src/share/classes/javax/management/QualifiedAttributeValueExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/QualifiedAttributeValueExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -27,9 +27,11 @@
 
 
 /**
- * This class represents indexed attributes used as arguments to relational
- * constraints. An QualifiedAttributeValueExp may be used anywhere a
- * ValueExp is required.
+ * <p>Represents attributes used as arguments to relational constraints,
+ * where the attribute must be in an MBean of a specified {@linkplain
+ * MBeanInfo#getClassName() class}. A QualifiedAttributeValueExp may be used
+ * anywhere a ValueExp is required.
+ *
  * @serial include
  *
  * @since 1.5
@@ -48,7 +50,9 @@
 
     /**
      * Basic Constructor.
+     * @deprecated see {@link AttributeValueExp#AttributeValueExp()}
      */
+    @Deprecated
     public QualifiedAttributeValueExp() {
     }
 
@@ -81,6 +85,7 @@
      * @exception BadAttributeValueExpException
      * @exception InvalidApplicationException
      */
+    @Override
     public ValueExp apply(ObjectName name) throws BadStringOperationException, BadBinaryOpValueExpException,
         BadAttributeValueExpException, InvalidApplicationException  {
         try {
@@ -105,9 +110,11 @@
     /**
      * Returns the string representing its value
      */
+    @Override
     public String toString()  {
         if (className != null) {
-            return className + "." + super.toString();
+            return QueryParser.quoteId(className) + "#" +
+                QueryParser.quoteId(super.toString());
         } else {
             return super.toString();
         }
--- a/jdk/src/share/classes/javax/management/Query.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/Query.java	Mon Mar 03 10:32:38 2008 +0100
@@ -27,19 +27,346 @@
 
 
 /**
- * <p>Constructs query object constraints. The static methods provided
- * return query expressions that may be used in listing and
- * enumerating MBeans. Individual constraint construction methods
- * allow only appropriate types as arguments. Composition of calls can
- * construct arbitrary nestings of constraints, as the following
- * example illustrates:</p>
+ * <p>Constructs query object constraints.</p>
+ *
+ * <p>The MBean Server can be queried for MBeans that meet a particular
+ * condition, using its {@link MBeanServer#queryNames queryNames} or
+ * {@link MBeanServer#queryMBeans queryMBeans} method.  The {@link QueryExp}
+ * parameter to the method can be any implementation of the interface
+ * {@code QueryExp}, but it is usually best to obtain the {@code QueryExp}
+ * value by calling the static methods in this class.  This is particularly
+ * true when querying a remote MBean Server: a custom implementation of the
+ * {@code QueryExp} interface might not be present in the remote MBean Server,
+ * but the methods in this class return only standard classes that are
+ * part of the JMX implementation.</p>
+ *
+ * <p>There are two ways to create {@code QueryExp} objects using the methods
+ * in this class.  The first is to build them by chaining together calls to
+ * the various methods.  The second is to use the Query Language described
+ * <a href="#ql">below</a> and produce the {@code QueryExp} by calling
+ * {@link #fromString Query.fromString}.  The two ways are equivalent:
+ * every {@code QueryExp} returned by {@code fromString} can also be
+ * constructed by chaining method calls.</p>
+ *
+ * <p>As an example, suppose you wanted to find all MBeans where the {@code
+ * Enabled} attribute is {@code true} and the {@code Owner} attribute is {@code
+ * "Duke"}. Here is how you could construct the appropriate {@code QueryExp} by
+ * chaining together method calls:</p>
+ *
+ * <pre>
+ * QueryExp query =
+ *     Query.and(Query.eq(Query.attr("Enabled"), Query.value(true)),
+ *               Query.eq(Query.attr("Owner"), Query.value("Duke")));
+ * </pre>
+ *
+ * <p>Here is how you could construct the same {@code QueryExp} using the
+ * Query Language:</p>
  *
  * <pre>
- * QueryExp exp = Query.and(Query.gt(Query.attr("age"),Query.value(5)),
- *                          Query.match(Query.attr("name"),
- *                                      Query.value("Smith")));
+ * QueryExp query = Query.fromString("Enabled = true and Owner = 'Duke'");
  * </pre>
  *
+ * <p>The principal advantage of the method-chaining approach is that the
+ * compiler will check that the query makes sense.  The principal advantage
+ * of the Query Language approach is that it is easier to write and especially
+ * read.</p>
+ *
+ *
+ * <h4 id="ql">Query Language</h4>
+ *
+ * <p>The query language is closely modeled on the WHERE clause of
+ * SQL SELECT statements. The formal specification of the language
+ * appears <a href="#formal-ql">below</a>, but it is probably easier to
+ * understand it with examples such as the following.</p>
+ *
+ * <dl>
+ * <dt>{@code Message = 'OK'}
+ * <dd>Selects MBeans that have a {@code Message} attribute whose value
+ *     is the string {@code OK}.
+ *
+ * <dt>{@code FreeSpacePercent < 10}
+ * <dd>Selects MBeans that have a {@code FreeSpacePercent} attribute whose
+ *     value is a number less than 10.
+ *
+ * <dt>{@code FreeSpacePercent < 10 and WarningSent = false}
+ * <dd>Selects the same MBeans as the previous example, but they must
+ *     also have a boolean attribute {@code WarningSent} whose value
+ *     is false.
+ *
+ * <dt>{@code SpaceUsed > TotalSpace * (2.0 / 3.0)}
+ * <dd>Selects MBeans that have {@code SpaceUsed} and {@code TotalSpace}
+ *     attributes where the first is more than two-thirds the second.
+ *
+ * <dt>{@code not (FreeSpacePercent between 10 and 90)}
+ * <dd>Selects MBeans that have a {@code FreeSpacePercent} attribute whose
+ *     value is not between 10 and 90, inclusive.
+ *
+ * <dt>{@code FreeSpacePercent not between 10 and 90}
+ * <dd>Another way of writing the previous query.
+ *
+ * <dt>{@code Status in ('STOPPED', 'STARTING', 'STARTED')}
+ * <dd>Selects MBeans that have a {@code Status} attribute whose value
+ *     is one of those three strings.
+ *
+ * <dt>{@code Message like 'OK: %'}
+ * <dd>Selects MBeans that have a {@code Message} attribute whose value
+ *     is a string beginning with {@code "OK: "}.  <b>Notice that the
+ *     wildcard characters are SQL's ones.</b>  In the query language,
+ *     {@code %} means "any sequence of characters" and {@code _}
+ *     means "any single character".  In the rest of the JMX API, these
+ *     correspond to {@code *} and {@code %} respectively.
+ *
+ * <dt>{@code instanceof 'javax.management.NotificationBroadcaster'}
+ * <dd>Selects MBeans that are instances of
+ *     {@link javax.management.NotificationBroadcaster}, as reported by
+ *     {@link javax.management.MBeanServer#isInstanceOf MBeanServer.isInstanceOf}.
+ *
+ * <dt>{@code like 'mydomain:*'}
+ * <dd>Selects MBeans whose {@link ObjectName}s have the domain {@code mydomain}.
+ *
+ * </dl>
+ *
+ * <p>The last two examples do not correspond to valid SQL syntax, but all
+ * the others do.</p>
+ *
+ * <p>The remainder of this description is a formal specification of the
+ * query language.</p>
+ *
+ *
+ * <h4 id="formal-ql">Lexical elements</h4>
+ *
+ * <p>Keywords such as <b>and</b>, <b>like</b>, and <b>between</b> are not
+ * case sensitive.  You can write <b>between</b>, <b>BETWEEN</b>, or
+ * <b>BeTwEeN</b> with the same effect.</p>
+ *
+ * <p>On the other hand, attribute names <i>are</i> case sensitive.  The
+ * attribute {@code Name} is not the same as the attribute {@code name}.</p>
+ *
+ * <p>To access an attribute whose name, ignoring case, is the same as one of
+ * the keywords {@code not}, {@code instanceof}, {@code like}, {@code true},
+ * or {@code false}, you can use double quotes, for example {@code "not"}.
+ * Double quotes can also be used to include non-identifier characters in
+ * the name of an attribute, for example {@code "attribute-name-with-hyphens"}.
+ * To include the double quote character in the attribute name, write it
+ * twice.  {@code "foo""bar""baz"} represents the attribute called
+ * {@code foo"bar"baz}.
+ *
+ * <p>String constants are written with single quotes like {@code 'this'}.  A
+ * single quote within a string constant must be doubled, for example
+ * {@code 'can''t'}.</p>
+ *
+ * <p>Integer constants are written as a sequence of decimal digits,
+ * optionally preceded by a plus or minus sign.  An integer constant must be
+ * a valid input to {@link Long#valueOf(String)}.</p>
+ *
+ * <p>Floating-point constants are written using the Java syntax.  A
+ * floating-point constant must be a valid input to
+ * {@link Double#valueOf(String)}.</p>
+ *
+ * <p>A boolean constant is either {@code true} or {@code false}, ignoring
+ * case.</p>
+ *
+ * <p>Spaces cannot appear inside identifiers (unless written with double
+ * quotes) or keywords or multi-character tokens such as {@code <=}. Spaces can
+ * appear anywhere else, but are not required except to separate tokens. For
+ * example, the query {@code a < b and 5 = c} could also be written {@code a<b
+ * and 5=c}, but no further spaces can be removed.</p>
+ *
+ *
+ * <h4 id="grammar-ql">Grammar</h4>
+ *
+ * <dl>
+ * <dt id="query">query:
+ * <dd><a href="#andquery">andquery</a> [<b>OR</b> <a href="#query">query</a>]
+ *
+ * <dt id="andquery">andquery:
+ * <dd><a href="#predicate">predicate</a> [<b>AND</b> <a href="#andquery">andquery</a>]
+ *
+ * <dt id="predicate">predicate:
+ * <dd><b>(</b> <a href="#query">query</a> <b>)</b> |<br>
+ *     <b>NOT</b> <a href="#predicate">predicate</a> |<br>
+ *     <b>INSTANCEOF</b> <a href="#stringvalue">stringvalue</a> |<br>
+ *     <b>LIKE</b> <a href="#objectnamepattern">objectnamepattern</a> |<br>
+ *     <a href="#value">value</a> <a href="#predrhs">predrhs</a>
+ *
+ * <dt id="predrhs">predrhs:
+ * <dd><a href="#compare">compare</a> <a href="#value">value</a> |<br>
+ *     [<b>NOT</b>] <b>BETWEEN</b> <a href="#value">value</a> <b>AND</b>
+ *         <a href="#value">value</a> |<br>
+ *     [<b>NOT</b>] <b>IN (</b> <a href="#value">value</a>
+ *           <a href="#commavalues">commavalues</a> <b>)</b> |<br>
+ *     [<b>NOT</b>] <b>LIKE</b> <a href="#stringvalue">stringvalue</a>
+ *
+ * <dt id="commavalues">commavalues:
+ * <dd>[ <b>,</b> <a href="#value">value</a> <a href="#commavalues">commavalues</a> ]
+ *
+ * <dt id="compare">compare:
+ * <dd><b>=</b> | <b>&lt;</b> | <b>&gt;</b> |
+ *     <b>&lt;=</b> | <b>&gt;=</b> | <b>&lt;&gt;</b> | <b>!=</b>
+ *
+ * <dt id="value">value:
+ * <dd><a href="#factor">factor</a> [<a href="#plusorminus">plusorminus</a>
+ *     <a href="#value">value</a>]
+ *
+ * <dt id="plusorminus">plusorminus:
+ * <dd><b>+</b> | <b>-</b>
+ *
+ * <dt id="factor">factor:
+ * <dd><a href="#term">term</a> [<a href="#timesordivide">timesordivide</a>
+ *     <a href="#factor">factor</a>]
+ *
+ * <dt id="timesordivide">timesordivide:
+ * <dd><b>*</b> | <b>/</b>
+ *
+ * <dt id="term">term:
+ * <dd><a href="#attr">attr</a> | <a href="#literal">literal</a> |
+ *     <b>(</b> <a href="#value">value</a> <b>)</b>
+ *
+ * <dt id="attr">attr:
+ * <dd><a href="#name">name</a> [<b>#</b> <a href="#name">name</a>]
+ *
+ * <dt id="name">name:
+ * <dd><a href="#identifier">identifier</a> [<b>.</b><a href="#name">name</a>]
+ *
+ * <dt id="identifier">identifier:
+ * <dd><i>Java-identifier</i> | <i>double-quoted-identifier</i>
+ *
+ * <dt id="literal">literal:
+ * <dd><a href="#booleanlit">booleanlit</a> | <i>longlit</i> |
+ *     <i>doublelit</i> | <i>stringlit</i>
+ *
+ * <dt id="booleanlit">booleanlit:
+ * <dd><b>FALSE</b> | <b>TRUE</b>
+ *
+ * <dt id="stringvalue">stringvalue:
+ * <dd><i>stringlit</i>
+ *
+ * <dt id="objectnamepattern">objectnamepattern:
+ * <dd><i>stringlit</i>
+ *
+ * </dl>
+ *
+ *
+ * <h4>Semantics</h4>
+ *
+ * <p>The meaning of the grammar is described in the table below.
+ * This defines a function <i>q</i> that maps a string to a Java object
+ * such as a {@link QueryExp} or a {@link ValueExp}.</p>
+ *
+ * <table border="1" cellpadding="5">
+ * <tr><th>String <i>s</i></th><th><i>q(s)</th></tr>
+ *
+ * <tr><td><i>query1</i> <b>OR</b> <i>query2</i>
+ *     <td>{@link Query#or Query.or}(<i>q(query1)</i>, <i>q(query2)</i>)
+ *
+ * <tr><td><i>query1</i> <b>AND</b> <i>query2</i>
+ *     <td>{@link Query#and Query.and}(<i>q(query1)</i>, <i>q(query2)</i>)
+ *
+ * <tr><td><b>(</b> <i>queryOrValue</i> <b>)</b>
+ *     <td><i>q(queryOrValue)</i>
+ *
+ * <tr><td><b>NOT</b> <i>query</i>
+ *     <td>{@link Query#not Query.not}(<i>q(query)</i>)
+ *
+ * <tr><td><b>INSTANCEOF</b> <i>stringLiteral</i>
+ *     <td>{@link Query#isInstanceOf Query.isInstanceOf}(<!--
+ * -->{@link Query#value(String) Query.value}(<i>q(stringLiteral)</i>))
+ *
+ * <tr><td><b>LIKE</b> <i>stringLiteral</i>
+ *     <td>{@link ObjectName#ObjectName(String) new ObjectName}(<!--
+ * --><i>q(stringLiteral)</i>)
+ *
+ * <tr><td><i>value1</i> <b>=</b> <i>value2</i>
+ *     <td>{@link Query#eq Query.eq}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>&lt;</b> <i>value2</i>
+ *     <td>{@link Query#lt Query.lt}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>&gt;</b> <i>value2</i>
+ *     <td>{@link Query#gt Query.gt}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>&lt;=</b> <i>value2</i>
+ *     <td>{@link Query#leq Query.leq}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>&gt;=</b> <i>value2</i>
+ *     <td>{@link Query#geq Query.geq}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>&lt;&gt;</b> <i>value2</i>
+ *     <td>{@link Query#not Query.not}({@link Query#eq Query.eq}(<!--
+ * --><i>q(value1)</i>, <i>q(value2)</i>))
+ *
+ * <tr><td><i>value1</i> <b>!=</b> <i>value2</i>
+ *     <td>{@link Query#not Query.not}({@link Query#eq Query.eq}(<!--
+ * --><i>q(value1)</i>, <i>q(value2)</i>))
+ *
+ * <tr><td><i>value1</i> <b>BETWEEN</b> <i>value2</i> AND <i>value3</i>
+ *     <td>{@link Query#between Query.between}(<i>q(value1)</i>,
+ *         <i>q(value2)</i>, <i>q(value3)</i>)
+ *
+ * <tr><td><i>value1</i> <b>NOT BETWEEN</b> <i>value2</i> AND <i>value3</i>
+ *     <td>{@link Query#not Query.not}({@link Query#between Query.between}(<!--
+ * --><i>q(value1)</i>, <i>q(value2)</i>, <i>q(value3)</i>))
+ *
+ * <tr><td><i>value1</i> <b>IN (</b> <i>value2</i>, <i>value3</i> <b>)</b>
+ *     <td>{@link Query#in Query.in}(<i>q(value1)</i>,
+ *         <code>new ValueExp[] {</code>
+ *         <i>q(value2)</i>, <i>q(value3)</i><code>}</code>)
+ *
+ * <tr><td><i>value1</i> <b>NOT IN (</b> <i>value2</i>, <i>value3</i> <b>)</b>
+ *     <td>{@link Query#not Query.not}({@link Query#in Query.in}(<i>q(value1)</i>,
+ *         <code>new ValueExp[] {</code>
+ *         <i>q(value2)</i>, <i>q(value3)</i><code>}</code>))
+ *
+ * <tr><td><i>value</i> <b>LIKE</b> <i>stringLiteral</i>
+ *     <td>{@link Query#match Query.match}(<i>q(value)</i>,
+ *         <i><a href="#translateWildcards">translateWildcards</a>(q(stringLiteral))</i>)
+ *
+ * <tr><td><i>value</i> <b>NOT LIKE</b> <i>stringLiteral</i>
+ *     <td>{@link Query#not Query.not}({@link Query#match Query.match}(<i>q(value)</i>,
+ *         <i><a href="#translateWildcards">translateWildcards</a>(q(stringLiteral))</i>))
+ *
+ * <tr><td><i>value1</i> <b>+</b> <i>value2</i>
+ *     <td>{@link Query#plus Query.plus}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>-</b> <i>value2</i>
+ *     <td>{@link Query#minus Query.minus}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>*</b> <i>value2</i>
+ *     <td>{@link Query#times Query.times}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>value1</i> <b>/</b> <i>value2</i>
+ *     <td>{@link Query#div Query.div}(<i>q(value1)</i>, <i>q(value2)</i>)
+ *
+ * <tr><td><i>name</i>
+ *     <td>{@link Query#attr(String) Query.attr}(<i>q(name)</i>)
+ *
+ * <tr><td><i>name1<b>#</b>name2</i>
+ *     <td>{@link Query#attr(String,String) Query.attr}(<i>q(name1)</i>,
+ *         <i>q(name2)</i>)
+ *
+ * <tr><td><b>FALSE</b>
+ *     <td>{@link Query#value(boolean) Query.value}(false)
+ *
+ * <tr><td><b>TRUE</b>
+ *     <td>{@link Query#value(boolean) Query.value}(true)
+ *
+ * <tr><td><i>decimalLiteral</i>
+ *     <td>{@link Query#value(long) Query.value}(<!--
+ * -->{@link Long#valueOf(String) Long.valueOf}(<i>decimalLiteral</i>))
+ *
+ * <tr><td><i>floatingPointLiteral</i>
+ *     <td>{@link Query#value(double) Query.value}(<!--
+ * -->{@link Double#valueOf(String) Double.valueOf}(<!--
+ * --><i>floatingPointLiteral</i>))
+ * </table>
+ *
+ * <p id="translateWildcards">Here, <i>translateWildcards</i> is a function
+ * that translates from the SQL notation for wildcards, using {@code %} and
+ * {@code _}, to the JMX API notation, using {@code *} and {@code ?}.  If the
+ * <b>LIKE</b> string already contains {@code *} or {@code ?}, these characters
+ * have their literal meanings, and will be quoted in the call to
+ * {@link Query#match Query.match}.</p>
+ *
  * @since 1.5
  */
  public class Query extends Object   {
@@ -277,16 +604,12 @@
      }
 
      /**
-      * <p>Returns a new attribute expression.</p>
-      *
-      * <p>Evaluating this expression for a given
-      * <code>objectName</code> includes performing {@link
-      * MBeanServer#getAttribute MBeanServer.getAttribute(objectName,
-      * name)}.</p>
+      * <p>Returns a new attribute expression.  See {@link AttributeValueExp}
+      * for a detailed description of the semantics of the expression.</p>
       *
       * @param name The name of the attribute.
       *
-      * @return  An attribute expression for the attribute named name.
+      * @return An attribute expression for the attribute named {@code name}.
       */
      public static AttributeValueExp attr(String name)  {
          return new AttributeValueExp(name);
@@ -628,6 +951,63 @@
      }
 
      /**
+      * <p>Return a string representation of the given query.  The string
+      * returned by this method can be converted back into an equivalent
+      * query using {@link #fromString fromString}.</p>
+      *
+      * <p>(Two queries are equivalent if they produce the same result in
+      * all cases.  Equivalent queries are not necessarily identical:
+      * for example the queries {@code Query.lt(Query.attr("A"), Query.attr("B"))}
+      * and {@code Query.not(Query.ge(Query.attr("A"), Query.attr("B")))} are
+      * equivalent but not identical.)</p>
+      *
+      * <p>The string returned by this method is only guaranteed to be converted
+      * back into an equivalent query if {@code query} was constructed, or
+      * could have been constructed, using the methods of this class.
+      * If you make a custom query {@code myQuery} by implementing
+      * {@link QueryExp} yourself then the result of
+      * {@code Query.toString(myQuery)} is unspecified.</p>
+      *
+      * @param query the query to convert.  If it is null, the result will
+      * also be null.
+      * @return the string representation of the query, or null if the
+      * query is null.
+      *
+      * @since 1.7
+      */
+     public static String toString(QueryExp query) {
+         if (query == null)
+             return null;
+
+         if (query instanceof ToQueryString)
+             return ((ToQueryString) query).toQueryString();
+
+         return query.toString();
+     }
+
+     /**
+      * <p>Produce a query from the given string.  The query returned
+      * by this method can be converted back into a string using
+      * {@link #toString(QueryExp) toString}.  The resultant string will
+      * not necessarily be equal to {@code s}.</p>
+      *
+      * @param s the string to convert.
+      *
+      * @return a {@code QueryExp} derived by parsing the string, or
+      * null if the string is null.
+      *
+      * @throws IllegalArgumentException if the string is not a valid
+      * query string.
+      *
+      * @since 1.7
+      */
+     public static QueryExp fromString(String s) {
+         if (s == null)
+             return null;
+         return new QueryParser(s).parseQuery();
+     }
+
+     /**
       * Utility method to escape strings used with
       * Query.{initial|any|final}SubString() methods.
       */
--- a/jdk/src/share/classes/javax/management/QueryEval.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/QueryEval.java	Mon Mar 03 10:32:38 2008 +0100
@@ -38,7 +38,7 @@
  *
  * @since 1.5
  */
-public abstract class QueryEval implements Serializable   {
+public abstract class QueryEval extends ToQueryString implements Serializable {
 
     /* Serial version */
     private static final long serialVersionUID = 2675899265640874796L;
--- a/jdk/src/share/classes/javax/management/QueryExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/QueryExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -30,9 +30,9 @@
 
 
 /**
- * <p>Represents relational constraints that can be used in database
- * query "where clauses".  Instances of QueryExp are returned by the
- * static methods of the {@link Query} class.</p>
+ * <p>Represents relational constraints similar to database query "where
+ * clauses". Instances of QueryExp are returned by the static methods of the
+ * {@link Query} class.</p>
  *
  * <p>It is possible, but not
  * recommended, to create custom queries by implementing this
@@ -40,6 +40,7 @@
  * QueryEval} class than to implement the interface directly, so that
  * the {@link #setMBeanServer} method works correctly.
  *
+ * @see MBeanServer#queryNames MBeanServer.queryNames
  * @since 1.5
  */
 public interface QueryExp extends Serializable {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/javax/management/QueryParser.java	Mon Mar 03 10:32:38 2008 +0100
@@ -0,0 +1,663 @@
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package javax.management;
+
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * <p>Parser for JMX queries represented in an SQL-like language.</p>
+ */
+/*
+ * Note that if a query starts with ( then we don't know whether it is
+ * a predicate or just a value that is parenthesized.  So, inefficiently,
+ * we try to parse a predicate and if that doesn't work we try to parse
+ * a value.
+ */
+class QueryParser {
+    // LEXER STARTS HERE
+
+    private static class Token {
+        final String string;
+        Token(String s) {
+            this.string = s;
+        }
+
+        @Override
+        public String toString() {
+            return string;
+        }
+    }
+
+    private static final Token
+            END = new Token("<end of string>"),
+            LPAR = new Token("("), RPAR = new Token(")"),
+            COMMA = new Token(","), DOT = new Token("."), SHARP = new Token("#"),
+            PLUS = new Token("+"), MINUS = new Token("-"),
+            TIMES = new Token("*"), DIVIDE = new Token("/"),
+            LT = new Token("<"), GT = new Token(">"),
+            LE = new Token("<="), GE = new Token(">="),
+            NE = new Token("<>"), EQ = new Token("="),
+            NOT = new Id("NOT"), INSTANCEOF = new Id("INSTANCEOF"),
+            FALSE = new Id("FALSE"), TRUE = new Id("TRUE"),
+            BETWEEN = new Id("BETWEEN"), AND = new Id("AND"),
+            OR = new Id("OR"), IN = new Id("IN"),
+            LIKE = new Id("LIKE"), CLASS = new Id("CLASS");
+
+    // Keywords that can appear where an identifier can appear.
+    // If an attribute is one of these, then it must be quoted when
+    // converting a query into a string.
+    // We use a TreeSet so we can look up case-insensitively.
+    private static final Set<String> idKeywords =
+            new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+    static {
+        for (Token t : new Token[] {NOT, INSTANCEOF, FALSE, TRUE, LIKE, CLASS})
+            idKeywords.add(t.string);
+    };
+
+    public static String quoteId(String id) {
+        if (id.contains("\"") || idKeywords.contains(id))
+            return '"' + id.replace("\"", "\"\"") + '"';
+        else
+            return id;
+    }
+
+    private static class Id extends Token {
+        Id(String id) {
+            super(id);
+        }
+
+        // All other tokens use object identity, which means e.g. that one
+        // occurrence of the string constant 'x' is not the same as another.
+        // For identifiers, we ignore case when testing for equality so that
+        // for a keyword such as AND you can also spell it as "And" or "and".
+        // But we keep the original case of the identifier, so if it's not
+        // a keyword we will distinguish between the attribute Foo and the
+        // attribute FOO.
+        @Override
+        public boolean equals(Object o) {
+            return (o instanceof Id && (((Id) o).toString().equalsIgnoreCase(toString())));
+        }
+    }
+
+    private static class QuotedId extends Token {
+        QuotedId(String id) {
+            super(id);
+        }
+
+        @Override
+        public String toString() {
+            return '"' + string.replace("\"", "\"\"") + '"';
+        }
+    }
+
+    private static class StringLit extends Token {
+        StringLit(String s) {
+            super(s);
+        }
+
+        @Override
+        public String toString() {
+            return '\'' + string.replace("'", "''") + '\'';
+        }
+    }
+
+    private static class LongLit extends Token {
+        long number;
+
+        LongLit(long number) {
+            super(Long.toString(number));
+            this.number = number;
+        }
+    }
+
+    private static class DoubleLit extends Token {
+        double number;
+
+        DoubleLit(double number) {
+            super(Double.toString(number));
+            this.number = number;
+        }
+    }
+
+    private static class Tokenizer {
+        private final String s;
+        private final int len;
+        private int i = 0;
+
+        Tokenizer(String s) {
+            this.s = s;
+            this.len = s.length();
+        }
+
+        private int thisChar() {
+            if (i == len)
+                return -1;
+            return s.codePointAt(i);
+        }
+
+        private void advance() {
+            i += Character.charCount(thisChar());
+        }
+
+        private int thisCharAdvance() {
+            int c = thisChar();
+            advance();
+            return c;
+        }
+
+        Token nextToken() {
+            // In this method, c is the character we're looking at, and
+            // thisChar() is the character after that.  Everything must
+            // preserve these invariants.  When we return we then have
+            // thisChar() being the start of the following token, so
+            // the next call to nextToken() will begin from there.
+            int c;
+
+            // Skip space
+            do {
+                if (i == len)
+                    return null;
+                c = thisCharAdvance();
+            } while (Character.isWhitespace(c));
+
+            // Now c is the first character of the token, and tokenI points
+            // to the character after that.
+            switch (c) {
+                case '(': return LPAR;
+                case ')': return RPAR;
+                case ',': return COMMA;
+                case '.': return DOT;
+                case '#': return SHARP;
+                case '*': return TIMES;
+                case '/': return DIVIDE;
+                case '=': return EQ;
+                case '-': return MINUS;
+                case '+': return PLUS;
+
+                case '>':
+                    if (thisChar() == '=') {
+                        advance();
+                        return GE;
+                    } else
+                        return GT;
+
+                case '<':
+                    c = thisChar();
+                    switch (c) {
+                        case '=': advance(); return LE;
+                        case '>': advance(); return NE;
+                        default: return LT;
+                    }
+
+                case '!':
+                    if (thisCharAdvance() != '=')
+                        throw new IllegalArgumentException("'!' must be followed by '='");
+                    return NE;
+
+                case '"':
+                case '\'': {
+                    int quote = c;
+                    StringBuilder sb = new StringBuilder();
+                    while (true) {
+                        while ((c = thisChar()) != quote) {
+                            if (c < 0) {
+                                throw new IllegalArgumentException(
+                                        "Unterminated string constant");
+                            }
+                            sb.appendCodePoint(thisCharAdvance());
+                        }
+                        advance();
+                        if (thisChar() == quote) {
+                            sb.appendCodePoint(quote);
+                            advance();
+                        } else
+                            break;
+                    }
+                    if (quote == '\'')
+                        return new StringLit(sb.toString());
+                    else
+                        return new QuotedId(sb.toString());
+                }
+            }
+
+            // Is it a numeric constant?
+            if (Character.isDigit(c) || c == '.') {
+                StringBuilder sb = new StringBuilder();
+                int lastc = -1;
+                while (true) {
+                    sb.appendCodePoint(c);
+                    c = Character.toLowerCase(thisChar());
+                    if (c == '+' || c == '-') {
+                        if (lastc != 'e')
+                            break;
+                    } else if (!Character.isDigit(c) && c != '.' && c != 'e')
+                        break;
+                    lastc = c;
+                    advance();
+                }
+                String s = sb.toString();
+                if (s.indexOf('.') >= 0 || s.indexOf('e') >= 0) {
+                    double d = parseDoubleCheckOverflow(s);
+                    return new DoubleLit(d);
+                } else {
+                    // Like the Java language, we allow the numeric constant
+                    // x where -x = Long.MIN_VALUE, even though x is not
+                    // representable as a long (it is Long.MAX_VALUE + 1).
+                    // Code in the parser will reject this value if it is
+                    // not the operand of unary minus.
+                    long l = -Long.parseLong("-" + s);
+                    return new LongLit(l);
+                }
+            }
+
+            // It must be an identifier.
+            if (!Character.isJavaIdentifierStart(c)) {
+                StringBuilder sb = new StringBuilder();
+                Formatter f = new Formatter(sb);
+                f.format("Bad character: %c (%04x)", c, c);
+                throw new IllegalArgumentException(sb.toString());
+            }
+
+            StringBuilder id = new StringBuilder();
+            while (true) { // identifier
+                id.appendCodePoint(c);
+                c = thisChar();
+                if (!Character.isJavaIdentifierPart(c))
+                    break;
+                advance();
+            }
+
+            return new Id(id.toString());
+        }
+    }
+
+    /* Parse a double as a Java compiler would do it, throwing an exception
+     * if the input does not fit in a double.  We assume that the input
+     * string is not "Infinity" and does not have a leading sign.
+     */
+    private static double parseDoubleCheckOverflow(String s) {
+        double d = Double.parseDouble(s);
+        if (Double.isInfinite(d))
+            throw new NumberFormatException("Overflow: " + s);
+        if (d == 0.0) {  // Underflow checking is hard!  CR 6604864
+            String ss = s;
+            int e = s.indexOf('e');  // we already forced E to lowercase
+            if (e > 0)
+                ss = s.substring(0, e);
+            ss = ss.replace("0", "").replace(".", "");
+            if (!ss.isEmpty())
+                throw new NumberFormatException("Underflow: " + s);
+        }
+        return d;
+    }
+
+    // PARSER STARTS HERE
+
+    private final List<Token> tokens;
+    private int tokenI;
+    // The current token is always tokens[tokenI].
+
+    QueryParser(String s) {
+        // Construct the complete list of tokens immediately and append
+        // a sentinel (END).
+        tokens = new ArrayList<Token>();
+        Tokenizer tokenizer = new Tokenizer(s);
+        Token t;
+        while ((t = tokenizer.nextToken()) != null)
+            tokens.add(t);
+        tokens.add(END);
+    }
+
+    private Token current() {
+        return tokens.get(tokenI);
+    }
+
+    // If the current token is t, then skip it and return true.
+    // Otherwise, return false.
+    private boolean skip(Token t) {
+        if (t.equals(current())) {
+            tokenI++;
+            return true;
+        }
+        return false;
+    }
+
+    // If the current token is one of the ones in 'tokens', then skip it
+    // and return its index in 'tokens'.  Otherwise, return -1.
+    private int skipOne(Token... tokens) {
+        for (int i = 0; i < tokens.length; i++) {
+            if (skip(tokens[i]))
+                return i;
+        }
+        return -1;
+    }
+
+    // If the current token is t, then skip it and return.
+    // Otherwise throw an exception.
+    private void expect(Token t) {
+        if (!skip(t))
+            throw new IllegalArgumentException("Expected " + t + ", found " + current());
+    }
+
+    private void next() {
+        tokenI++;
+    }
+
+    QueryExp parseQuery() {
+        QueryExp qe = query();
+        if (current() != END)
+            throw new IllegalArgumentException("Junk at end of query: " + current());
+        return qe;
+    }
+
+    // The remainder of this class is a classical recursive-descent parser.
+    // We only need to violate the recursive-descent scheme in one place,
+    // where parentheses make the grammar not LL(1).
+
+    private QueryExp query() {
+        QueryExp lhs = andquery();
+        while (skip(OR))
+            lhs = Query.or(lhs, andquery());
+        return lhs;
+    }
+
+    private QueryExp andquery() {
+        QueryExp lhs = predicate();
+        while (skip(AND))
+            lhs = Query.and(lhs, predicate());
+        return lhs;
+    }
+
+    private QueryExp predicate() {
+        // Grammar hack.  If we see a paren, it might be (query) or
+        // it might be (value).  We try to parse (query), and if that
+        // fails, we parse (value).  For example, if the string is
+        // "(2+3)*4 < 5" then we will try to parse the query
+        // "2+3)*4 < 5", which will fail at the ), so we'll back up to
+        // the paren and let value() handle it.
+        if (skip(LPAR)) {
+            int parenIndex = tokenI - 1;
+            try {
+                QueryExp qe = query();
+                expect(RPAR);
+                return qe;
+            } catch (IllegalArgumentException e) {
+                // OK: try parsing a value
+            }
+            tokenI = parenIndex;
+        }
+
+        if (skip(NOT))
+            return Query.not(predicate());
+
+        if (skip(INSTANCEOF))
+            return Query.isInstanceOf(stringvalue());
+
+        if (skip(LIKE)) {
+            StringValueExp sve = stringvalue();
+            String s = sve.getValue();
+            try {
+                return new ObjectName(s);
+            } catch (MalformedObjectNameException e) {
+                throw new IllegalArgumentException(
+                        "Bad ObjectName pattern after LIKE: '" + s + "'", e);
+            }
+        }
+
+        ValueExp lhs = value();
+
+        return predrhs(lhs);
+    }
+
+    // The order of elements in the following arrays is important.  The code
+    // in predrhs depends on integer indexes.  Change with caution.
+    private static final Token[] relations = {
+            EQ, LT, GT, LE, GE, NE,
+         // 0,  1,  2,  3,  4,  5,
+    };
+    private static final Token[] betweenLikeIn = {
+            BETWEEN, LIKE, IN
+         // 0,       1,    2,
+    };
+
+    private QueryExp predrhs(ValueExp lhs) {
+        Token start = current(); // for errors
+
+        // Look for < > = etc
+        int i = skipOne(relations);
+        if (i >= 0) {
+            ValueExp rhs = value();
+            switch (i) {
+                case 0: return Query.eq(lhs, rhs);
+                case 1: return Query.lt(lhs, rhs);
+                case 2: return Query.gt(lhs, rhs);
+                case 3: return Query.leq(lhs, rhs);
+                case 4: return Query.geq(lhs, rhs);
+                case 5: return Query.not(Query.eq(lhs, rhs));
+                // There is no Query.ne so <> is shorthand for the above.
+                default:
+                    throw new AssertionError();
+            }
+        }
+
+        // Must be BETWEEN LIKE or IN, optionally preceded by NOT
+        boolean not = skip(NOT);
+        i = skipOne(betweenLikeIn);
+        if (i < 0)
+            throw new IllegalArgumentException("Expected relation at " + start);
+
+        QueryExp q;
+        switch (i) {
+            case 0: { // BETWEEN
+                ValueExp lower = value();
+                expect(AND);
+                ValueExp upper = value();
+                q = Query.between(lhs, lower, upper);
+                break;
+            }
+
+            case 1: { // LIKE
+                if (!(lhs instanceof AttributeValueExp)) {
+                    throw new IllegalArgumentException(
+                            "Left-hand side of LIKE must be an attribute");
+                }
+                AttributeValueExp alhs = (AttributeValueExp) lhs;
+                StringValueExp sve = stringvalue();
+                String s = sve.getValue();
+                q = Query.match(alhs, patternValueExp(s));
+                break;
+            }
+
+            case 2: { // IN
+                expect(LPAR);
+                List<ValueExp> values = new ArrayList<ValueExp>();
+                values.add(value());
+                while (skip(COMMA))
+                    values.add(value());
+                expect(RPAR);
+                q = Query.in(lhs, values.toArray(new ValueExp[values.size()]));
+                break;
+            }
+
+            default:
+                throw new AssertionError();
+        }
+
+        if (not)
+            q = Query.not(q);
+
+        return q;
+    }
+
+    private ValueExp value() {
+        ValueExp lhs = factor();
+        int i;
+        while ((i = skipOne(PLUS, MINUS)) >= 0) {
+            ValueExp rhs = factor();
+            if (i == 0)
+                lhs = Query.plus(lhs, rhs);
+            else
+                lhs = Query.minus(lhs, rhs);
+        }
+        return lhs;
+    }
+
+    private ValueExp factor() {
+        ValueExp lhs = term();
+        int i;
+        while ((i = skipOne(TIMES, DIVIDE)) >= 0) {
+            ValueExp rhs = term();
+            if (i == 0)
+                lhs = Query.times(lhs, rhs);
+            else
+                lhs = Query.div(lhs, rhs);
+        }
+        return lhs;
+    }
+
+    private ValueExp term() {
+        boolean signed = false;
+        int sign = +1;
+        if (skip(PLUS))
+            signed = true;
+        else if (skip(MINUS)) {
+            signed = true; sign = -1;
+        }
+
+        Token t = current();
+        next();
+
+        if (t instanceof DoubleLit)
+            return Query.value(sign * ((DoubleLit) t).number);
+        if (t instanceof LongLit) {
+            long n = ((LongLit) t).number;
+            if (n == Long.MIN_VALUE && sign != -1)
+                throw new IllegalArgumentException("Illegal positive integer: " + n);
+            return Query.value(sign * n);
+        }
+        if (signed)
+            throw new IllegalArgumentException("Expected number after + or -");
+
+        if (t == LPAR) {
+            ValueExp v = value();
+            expect(RPAR);
+            return v;
+        }
+        if (t.equals(FALSE) || t.equals(TRUE)) {
+            return Query.value(t.equals(TRUE));
+        }
+        if (t.equals(CLASS))
+            return Query.classattr();
+
+        if (t instanceof StringLit)
+            return Query.value(t.string); // Not toString(), which would requote '
+
+        // At this point, all that remains is something that will call Query.attr
+
+        if (!(t instanceof Id) && !(t instanceof QuotedId))
+            throw new IllegalArgumentException("Unexpected token " + t);
+
+        String name1 = name(t);
+
+        if (skip(SHARP)) {
+            Token t2 = current();
+            next();
+            String name2 = name(t2);
+            return Query.attr(name1, name2);
+        }
+        return Query.attr(name1);
+    }
+
+    // Initially, t is the first token of a supposed name and current()
+    // is the second.
+    private String name(Token t) {
+        StringBuilder sb = new StringBuilder();
+        while (true) {
+            if (!(t instanceof Id) && !(t instanceof QuotedId))
+                throw new IllegalArgumentException("Unexpected token " + t);
+            sb.append(t.string);
+            if (current() != DOT)
+                break;
+            sb.append('.');
+            next();
+            t = current();
+            next();
+        }
+        return sb.toString();
+    }
+
+    private StringValueExp stringvalue() {
+        // Currently the only way to get a StringValueExp when constructing
+        // a QueryExp is via Query.value(String), so we only recognize
+        // string literals here.  But if we expand queries in the future
+        // that might no longer be true.
+        Token t = current();
+        next();
+        if (!(t instanceof StringLit))
+            throw new IllegalArgumentException("Expected string: " + t);
+        return Query.value(t.string);
+    }
+
+    // Convert the SQL pattern syntax, using % and _, to the Query.match
+    // syntax, using * and ?.  The tricky part is recognizing \% and
+    // \_ as literal values, and also not replacing them inside [].
+    // But Query.match does not recognize \ inside [], which makes our
+    // job a tad easier.
+    private StringValueExp patternValueExp(String s) {
+        int c;
+        for (int i = 0; i < s.length(); i += Character.charCount(c)) {
+            c = s.codePointAt(i);
+            switch (c) {
+                case '\\':
+                    i++;  // i += Character.charCount(c), but we know it's 1!
+                    if (i >= s.length())
+                        throw new IllegalArgumentException("\\ at end of pattern");
+                    break;
+                case '[':
+                    i = s.indexOf(']', i);
+                    if (i < 0)
+                        throw new IllegalArgumentException("[ without ]");
+                    break;
+                case '%':
+                    s = s.substring(0, i) + "*" + s.substring(i + 1);
+                    break;
+                case '_':
+                    s = s.substring(0, i) + "?" + s.substring(i + 1);
+                    break;
+                case '*':
+                case '?':
+                    s = s.substring(0, i) + '\\' + (char) c + s.substring(i + 1);
+                    i++;
+                    break;
+            }
+        }
+        return Query.value(s);
+    }
+}
--- a/jdk/src/share/classes/javax/management/StringValueExp.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/StringValueExp.java	Mon Mar 03 10:32:38 2008 +0100
@@ -73,7 +73,7 @@
      * Returns the string representing the object.
      */
     public String toString()  {
-        return "'" + val + "'";
+        return "'" + val.replace("'", "''") + "'";
     }
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/javax/management/ToQueryString.java	Mon Mar 03 10:32:38 2008 +0100
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package javax.management;
+
+/* QueryExp classes can extend this to get non-default treatment for
+ * Query.toString(q).  We're reluctant to change the public toString()
+ * methods of the classes because people might be parsing them, even
+ * though that's rather fragile.  But Query.toString(q) has no such
+ * constraint so it can use the new toQueryString() method defined here.
+ */
+class ToQueryString {
+    String toQueryString() {
+        return toString();
+    }
+}
--- a/jdk/src/share/classes/javax/management/monitor/Monitor.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/src/share/classes/javax/management/monitor/Monitor.java	Mon Mar 03 10:32:38 2008 +0100
@@ -27,13 +27,8 @@
 
 import static com.sun.jmx.defaults.JmxProperties.MONITOR_LOGGER;
 import com.sun.jmx.mbeanserver.GetPropertyAction;
-import com.sun.jmx.remote.util.EnvHelp;
-import java.beans.BeanInfo;
-import java.beans.Introspector;
-import java.beans.PropertyDescriptor;
+import com.sun.jmx.mbeanserver.Introspector;
 import java.io.IOException;
-import java.lang.reflect.Array;
-import java.lang.reflect.InvocationTargetException;
 import java.security.AccessControlContext;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
@@ -64,7 +59,6 @@
 import javax.management.ObjectName;
 import javax.management.ReflectionException;
 import static javax.management.monitor.MonitorNotification.*;
-import javax.management.openmbean.CompositeData;
 
 /**
  * Defines the part common to all monitor MBeans.
@@ -876,44 +870,13 @@
         if (isComplexTypeAttribute) {
             Object v = value;
             for (String attr : remainingAttributes)
-                v = introspect(object, attr, v);
+                v = Introspector.elementFromComplex(v, attr);
             return (Comparable<?>) v;
         } else {
             return (Comparable<?>) value;
         }
     }
 
-    Object introspect(ObjectName object,
-                      String attribute,
-                      Object value)
-        throws AttributeNotFoundException {
-        try {
-            if (value.getClass().isArray() && attribute.equals("length")) {
-                return Array.getLength(value);
-            } else if (value instanceof CompositeData) {
-                return ((CompositeData) value).get(attribute);
-            } else {
-                // Java Beans introspection
-                //
-                BeanInfo bi = Introspector.getBeanInfo(value.getClass());
-                PropertyDescriptor[] pds = bi.getPropertyDescriptors();
-                for (PropertyDescriptor pd : pds)
-                    if (pd.getName().equals(attribute))
-                        return pd.getReadMethod().invoke(value);
-                throw new AttributeNotFoundException(
-                    "Could not find the getter method for the property " +
-                    attribute + " using the Java Beans introspector");
-            }
-        } catch (InvocationTargetException e) {
-            throw new IllegalArgumentException(e);
-        } catch (AttributeNotFoundException e) {
-            throw e;
-        } catch (Exception e) {
-            throw EnvHelp.initCause(
-                new AttributeNotFoundException(e.getMessage()), e);
-        }
-    }
-
     boolean isComparableTypeValid(ObjectName object,
                                   String attribute,
                                   Comparable<?> value) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/management/query/QueryDottedAttrTest.java	Mon Mar 03 10:32:38 2008 +0100
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/*
+ * @test QueryDottedAttrTest
+ * @bug 6602310
+ * @summary Test that Query.attr can understand a.b etc.
+ * @author Eamonn McManus
+ */
+
+import java.beans.ConstructorProperties;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Collections;
+import java.util.Set;
+import javax.management.AttributeNotFoundException;
+import javax.management.MBeanException;
+import javax.management.MBeanServer;
+import javax.management.MBeanServerFactory;
+import javax.management.ObjectName;
+import javax.management.Query;
+import javax.management.QueryExp;
+import javax.management.ReflectionException;
+import javax.management.StandardMBean;
+
+public class QueryDottedAttrTest {
+    public static class Complex {
+        private final double re, im;
+
+        @ConstructorProperties({"real", "imaginary"})
+        public Complex(double re, double im) {
+            this.re = re;
+            this.im = im;
+        }
+
+        public double getRe() {
+            return re;
+        }
+
+        public double getIm() {
+            return im;
+        }
+    }
+
+    public static interface Intf {
+        Complex getComplex();
+        int[] getIntArray();
+        String[] getStringArray();
+    }
+
+    public static class Impl implements Intf {
+        public Complex getComplex() {
+            return new Complex(1.0, 1.0);
+        }
+
+        public int[] getIntArray() {
+            return new int[] {1, 2, 3};
+        }
+
+        public String[] getStringArray() {
+            return new String[] {"one", "two", "three"};
+        }
+    }
+
+    public static interface TestMBean extends Intf {}
+
+    public static class Test extends Impl implements TestMBean {}
+
+    public static interface TestMXBean extends Intf {}
+
+    public static class TestMX extends Impl implements TestMXBean {}
+
+    public static class AttrWithDot extends StandardMBean {
+        public <T> AttrWithDot(Object impl, Class<T> intf) {
+            super(intf.cast(impl), intf, (intf == TestMXBean.class));
+        }
+
+        public Object getAttribute(String attribute)
+        throws AttributeNotFoundException, MBeanException, ReflectionException {
+            if (attribute.equals("Complex.re"))
+                return 2.0;
+            else
+                return super.getAttribute(attribute);
+        }
+    }
+
+    private static final boolean[] booleans = {false, true};
+
+    private static final QueryExp[] alwaysTrueQueries = {
+        Query.eq(Query.attr("IntArray.length"), Query.value(3)),
+        Query.eq(Query.attr("StringArray.length"), Query.value(3)),
+        Query.eq(Query.attr("Complex.im"), Query.value(1.0)),
+    };
+
+    private static final QueryExp[] alwaysFalseQueries = {
+        Query.eq(Query.attr("IntArray.length"), Query.value("3")),
+        Query.eq(Query.attr("IntArray.length"), Query.value(2)),
+        Query.eq(Query.attr("Complex.im"), Query.value(-1.0)),
+        Query.eq(Query.attr("Complex.xxx"), Query.value(0)),
+    };
+
+    private static final QueryExp[] attrWithDotTrueQueries = {
+        Query.eq(Query.attr("Complex.re"), Query.value(2.0)),
+    };
+
+    private static final QueryExp[] attrWithDotFalseQueries = {
+        Query.eq(Query.attr("Complex.re"), Query.value(1.0)),
+    };
+
+    private static String failure;
+
+    public static void main(String[] args) throws Exception {
+        ObjectName name = new ObjectName("a:b=c");
+        for (boolean attrWithDot : booleans) {
+            for (boolean mx : booleans) {
+                String what =
+                        (mx ? "MXBean" : "Standard MBean") +
+                        (attrWithDot ? " having attribute with dot in its name" : "");
+                System.out.println("Testing " + what);
+                Class<?> intf = mx ? TestMXBean.class : TestMBean.class;
+                Object impl = mx ? new TestMX() : new Test();
+                if (attrWithDot)
+                    impl = new AttrWithDot(impl, intf);
+                MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+                mbs.registerMBean(impl, name);
+                boolean ismx = "true".equals(
+                        mbs.getMBeanInfo(name).getDescriptor().getFieldValue("mxbean"));
+                if (mx != ismx)
+                    fail("MBean should " + (mx ? "" : "not ") + "be MXBean");
+                test(mbs, name, alwaysTrueQueries, true);
+                test(mbs, name, alwaysFalseQueries, false);
+                test(mbs, name, attrWithDotTrueQueries, attrWithDot);
+                test(mbs, name, attrWithDotFalseQueries, !attrWithDot);
+            }
+        }
+        if (failure != null)
+            throw new Exception("TEST FAILED: " + failure);
+    }
+
+    private static void test(
+            MBeanServer mbs, ObjectName name, QueryExp[] queries, boolean expect)
+            throws Exception {
+        for (QueryExp query : queries) {
+            // Serialize and deserialize the query to ensure that its
+            // serialization is correct
+            ByteArrayOutputStream bout = new ByteArrayOutputStream();
+            ObjectOutputStream oout = new ObjectOutputStream(bout);
+            oout.writeObject(query);
+            oout.close();
+            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
+            ObjectInputStream oin = new ObjectInputStream(bin);
+            query = (QueryExp) oin.readObject();
+            Set<ObjectName> names = mbs.queryNames(null, query);
+            if (names.isEmpty()) {
+                if (expect)
+                    fail("Query is false but should be true: " + query);
+            } else if (names.equals(Collections.singleton(name))) {
+                if (!expect)
+                    fail("Query is true but should be false: " + query);
+            } else {
+                fail("Query returned unexpected set: " + names);
+            }
+        }
+    }
+
+    private static void fail(String msg) {
+        failure = msg;
+        System.out.println("..." + msg);
+    }
+}
--- a/jdk/test/javax/management/query/QueryExpStringTest.java	Fri Feb 29 20:04:01 2008 -0800
+++ b/jdk/test/javax/management/query/QueryExpStringTest.java	Mon Mar 03 10:32:38 2008 +0100
@@ -31,6 +31,10 @@
  * @run main QueryExpStringTest
  */
 
+// This test is mostly obsolete, since we now have Query.fromString.
+// The test includes its own parser, from which Query.fromString was derived.
+// The parsers are not identical and the one here is no longer maintained.
+
 import java.util.*;
 import javax.management.*;
 
@@ -39,6 +43,11 @@
     private static final ValueExp
         attr = Query.attr("attr"),
         qattr = Query.attr("className", "attr"),
+        aa = Query.attr("A"),
+        bb = Query.attr("B"),
+        cc = Query.attr("C"),
+        dd = Query.attr("D"),
+        zero = Query.value(0),
         classattr = Query.classattr(),
         simpleString = Query.value("simpleString"),
         complexString = Query.value("a'b\\'\""),
@@ -66,10 +75,14 @@
                                          (StringValueExp) simpleString),
         initialStar = Query.initialSubString((AttributeValueExp) attr,
                                              Query.value("*")),
+        initialPercent = Query.initialSubString((AttributeValueExp) attr,
+                                                Query.value("%")),
         any = Query.anySubString((AttributeValueExp) attr,
                                  (StringValueExp) simpleString),
         anyStar = Query.anySubString((AttributeValueExp) attr,
                                      Query.value("*")),
+        anyPercent = Query.anySubString((AttributeValueExp) attr,
+                                        Query.value("%")),
         ffinal = Query.finalSubString((AttributeValueExp) attr,
                                       (StringValueExp) simpleString),
         finalMagic = Query.finalSubString((AttributeValueExp) attr,
@@ -77,16 +90,20 @@
         in = Query.in(intValue, new ValueExp[] {intValue, floatValue}),
         and = Query.and(gt, lt),
         or = Query.or(gt, lt),
-        not = Query.not(gt);
+        not = Query.not(gt),
+        aPlusB_PlusC = Query.gt(Query.plus(Query.plus(aa, bb), cc), zero),
+        aPlus_BPlusC = Query.gt(Query.plus(aa, Query.plus(bb, cc)), zero);
 
     // Commented-out tests below require change to implementation
 
     private static final Object tests[] = {
         attr, "attr",
-        qattr, "className.attr",
+//      qattr, "className.attr",
+// Preceding form now appears as className#attr, an incompatible change
+// which we don't mind much because nobody uses the two-arg Query.attr.
         classattr, "Class",
         simpleString, "'simpleString'",
-//      complexString, "'a\\'b\\\\\\'\"'",
+        complexString, "'a''b\\\''\"'",
         intValue, "12345678",
         integerValue, "12345678",
         longValue, "12345678",
@@ -104,16 +121,20 @@
         eq, "(12345678) = (2.5)",
         between, "(12345678) between (2.5) and (2.5)",
         match, "attr like 'simpleString'",
-//      initial, "attr like 'simpleString*'",
-//      initialStar, "attr like '\\\\**'",
-//      any, "attr like '*simpleString*'",
-//      anyStar, "attr like '*\\\\**'",
-//      ffinal, "attr like '*simpleString'",
-//      finalMagic, "attr like '*\\\\?\\\\*\\\\[\\\\\\\\'",
+        initial, "attr like 'simpleString%'",
+        initialStar, "attr like '\\*%'",
+        initialPercent, "attr like '\\%%'",
+        any, "attr like '%simpleString%'",
+        anyStar, "attr like '%\\*%'",
+        anyPercent, "attr like '%\\%%'",
+        ffinal, "attr like '%simpleString'",
+        finalMagic, "attr like '%\\?\\*\\[\\\\'",
         in, "12345678 in (12345678, 2.5)",
         and, "((12345678) > (2.5)) and ((12345678) < (2.5))",
         or, "((12345678) > (2.5)) or ((12345678) < (2.5))",
         not, "not ((12345678) > (2.5))",
+        aPlusB_PlusC, "(A + B + C) > (0)",
+//        aPlus_BPlusC, "(A + (B + C)) > (0)",
     };
 
     public static void main(String[] args) throws Exception {
@@ -185,7 +206,9 @@
                 throw new Exception("Expected types `attr like string': " +
                                     exp + " like " + pat);
             }
-            return Query.match((AttributeValueExp) exp, (StringValueExp) pat);
+            StringValueExp spat = (StringValueExp) pat;
+            spat = Query.value(translateMatch(spat.getValue()));
+            return Query.match((AttributeValueExp) exp, spat);
         }
 
         if (skip(ss, " in (")) {
@@ -203,6 +226,28 @@
         throw new Exception("Expected in or like after expression");
     }
 
+    private static String translateMatch(String s) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < s.length(); i++) {  // logic not correct for wide chars
+            char c = s.charAt(i);
+            switch (c) {
+                case '\\':
+                    sb.append(c).append(s.charAt(++i)); break;
+                case '%':
+                    sb.append('*'); break;
+                case '_':
+                    sb.append('?'); break;
+                case '*':
+                    sb.append("\\*"); break;
+                case '?':
+                    sb.append("\\?"); break;
+                default:
+                    sb.append(c); break;
+            }
+        }
+        return sb.toString();
+    }
+
     private static QueryExp parseQueryAfterParen(String[] ss)
             throws Exception {
         /* This is very ugly.  We might have "(q1) and (q2)" here, or
@@ -229,7 +274,7 @@
             ss[0] = start;
             ValueExp lhs = parseExp(ss);
             if (!skip(ss, ") "))
-                throw new Exception("Expected `) ' after subexpression");
+                throw new Exception("Expected `) ' after subexpression: " + ss[0]);
             String op = scanWord(ss);
             if (!skip(ss, " ("))
                 throw new Exception("Expected ` (' after `" + op + "'");
@@ -258,15 +303,16 @@
     }
 
     private static ValueExp parseExp(String[] ss) throws Exception {
-        final ValueExp prim = parsePrimary(ss);
+        ValueExp lhs = parsePrimary(ss);
 
+        while (true) {
         /* Look ahead to see if we have an arithmetic operator. */
         String back = ss[0];
         if (!skip(ss, " "))
-            return prim;
+                return lhs;
         if (ss[0].equals("") || "+-*/".indexOf(ss[0].charAt(0)) < 0) {
             ss[0] = back;
-            return prim;
+                return lhs;
         }
 
         final String op = scanWord(ss);
@@ -276,15 +322,16 @@
             throw new Exception("Unknown arithmetic operator: " + op);
         if (!skip(ss, " "))
             throw new Exception("Expected space after arithmetic operator");
-        ValueExp rhs = parseExp(ss);
+            ValueExp rhs = parsePrimary(ss);
         switch (op.charAt(0)) {
-        case '+': return Query.plus(prim, rhs);
-        case '-': return Query.minus(prim, rhs);
-        case '*': return Query.times(prim, rhs);
-        case '/': return Query.div(prim, rhs);
+            case '+': lhs = Query.plus(lhs, rhs); break;
+            case '-': lhs = Query.minus(lhs, rhs); break;
+            case '*': lhs = Query.times(lhs, rhs); break;
+            case '/': lhs = Query.div(lhs, rhs); break;
         default: throw new Exception("Can't happen: " + op.charAt(0));
         }
     }
+    }
 
     private static ValueExp parsePrimary(String[] ss) throws Exception {
         String s = ss[0];
@@ -324,14 +371,19 @@
     private static String scanWord(String[] ss) throws Exception {
         String s = ss[0];
         int space = s.indexOf(' ');
-        if (space < 0) {
+        int rpar = s.indexOf(')');
+        if (space < 0 && rpar < 0) {
             ss[0] = "";
             return s;
-        } else {
-            String word = s.substring(0, space);
-            ss[0] = s.substring(space);
-            return word;
         }
+        int stop;
+        if (space >= 0 && rpar >= 0)  // string has both space and ), stop at first
+            stop = Math.min(space, rpar);
+        else                          // string has only one, stop at it
+            stop = Math.max(space, rpar);
+        String word = s.substring(0, stop);
+        ss[0] = s.substring(stop);
+        return word;
     }
 
     private static boolean matchWord(String[] ss, String word)
@@ -381,13 +433,11 @@
         for (i = 0; i < len; i++) {
             char c = s.charAt(i);
             if (c == '\'') {
-                ss[0] = s.substring(i + 1);
+                ++i;
+                if (i >= len || s.charAt(i) != '\'') {
+                    ss[0] = s.substring(i);
                 return Query.value(buf.toString());
             }
-            if (c == '\\') {
-                if (++i == len)
-                    throw new Exception("\\ at end of string");
-                c = s.charAt(i);
             }
             buf.append(c);
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/management/query/QueryParseTest.java	Mon Mar 03 10:32:38 2008 +0100
@@ -0,0 +1,778 @@
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/*
+ * @test QueryParseTest
+ * @bug 6602310 6604768
+ * @summary Test Query.fromString and Query.toString.
+ * @author Eamonn McManus
+ */
+
+import java.util.Collections;
+import java.util.Set;
+import javax.management.Attribute;
+import javax.management.AttributeList;
+import javax.management.AttributeNotFoundException;
+import javax.management.DynamicMBean;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanInfo;
+import javax.management.MBeanServer;
+import javax.management.MBeanServerDelegate;
+import javax.management.MBeanServerFactory;
+import javax.management.ObjectName;
+import javax.management.Query;
+import javax.management.QueryExp;
+
+public class QueryParseTest {
+    // In this table, each string constant corresponds to a test case.
+    // The objects following the string up to the next string are MBeans.
+    // Each MBean must implement ExpectedValue to return true or false
+    // according as it should return that value for the query parsed
+    // from the given string.  The test will parse the string into a
+    // a query and verify that the MBeans return the expected value
+    // for that query.  Then it will convert the query back into a string
+    // and into a second query, and check that the MBeans return the
+    // expected value for that query too.  The reason we need to do all
+    // this is that the spec talks about "equivalent queries", and gives
+    // the implementation wide scope to rearrange queries.  So we cannot
+    // just compare string values.
+    //
+    // We could also write an implementation-dependent test that knew what
+    // the strings look like, and that would have to be changed if the
+    // implementation changed.  But the approach here is cleaner.
+    //
+    // To simplify the creation of MBeans, most use the expectTrue or
+    // expectFalse methods.  The parameters of these methods end up in
+    // attributes called "A", "B", "C", etc.
+    private static final Object[] queryTests = {
+        // RELATIONS
+
+        "A < B",
+        expectTrue(1, 2), expectTrue(1.0, 2.0), expectTrue("one", "two"),
+        expectTrue(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY),
+        expectFalse(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY),
+        expectFalse(1, 1), expectFalse(1.0, 1.0), expectFalse("one", "one"),
+        expectFalse(2, 1), expectFalse(2.0, 1.0), expectFalse("two", "one"),
+        expectFalse(Double.NaN, Double.NaN),
+
+        "One = two",
+        expectTrueOneTwo(1, 1), expectTrueOneTwo(1.0, 1.0),
+        expectFalseOneTwo(1, 2), expectFalseOneTwo(2, 1),
+
+        "A <= B",
+        expectTrue(1, 1), expectTrue(1, 2), expectTrue("one", "one"),
+        expectTrue("one", "two"),
+        expectFalse(2, 1), expectFalse("two", "one"),
+        expectFalse(Double.NaN, Double.NaN),
+
+        "A >= B",
+        expectTrue(1, 1), expectTrue(2, 1), expectTrue("two", "one"),
+        expectFalse(1, 2), expectFalse("one", "two"),
+
+        "A > B",
+        expectTrue(2, 1), expectTrue("two", "one"),
+        expectFalse(2, 2), expectFalse(1, 2), expectFalse(1.0, 2.0),
+        expectFalse("one", "two"),
+
+        "A <> B",
+        expectTrue(1, 2), expectTrue("foo", "bar"),
+        expectFalse(1, 1), expectFalse("foo", "foo"),
+
+        "A != B",
+        expectTrue(1, 2), expectTrue("foo", "bar"),
+        expectFalse(1, 1), expectFalse("foo", "foo"),
+
+        // PARENTHESES
+
+        "(((A))) = (B)",
+        expectTrue(1, 1), expectFalse(1, 2),
+
+        "(A = B)",
+        expectTrue(1, 1), expectFalse(1, 2),
+
+        "(((A = (B))))",
+        expectTrue(1, 1), expectFalse(1, 2),
+
+        // INTEGER LITERALS
+
+        "A = 1234567890123456789",
+        expectTrue(1234567890123456789L), expectFalse(123456789L),
+
+        "A = +1234567890123456789",
+        expectTrue(1234567890123456789L), expectFalse(123456789L),
+
+        "A = -1234567890123456789",
+        expectTrue(-1234567890123456789L), expectFalse(-123456789L),
+
+
+        "A = + 1234567890123456789",
+        expectTrue(1234567890123456789L), expectFalse(123456789L),
+
+        "A = - 1234567890123456789",
+        expectTrue(-1234567890123456789L), expectFalse(-123456789L),
+
+        "A = " + Long.MAX_VALUE,
+        expectTrue(Long.MAX_VALUE), expectFalse(Long.MIN_VALUE),
+
+        "A = " + Long.MIN_VALUE,
+        expectTrue(Long.MIN_VALUE), expectFalse(Long.MAX_VALUE),
+
+        // DOUBLE LITERALS
+
+        "A = 0.0",
+        expectTrue(0.0), expectFalse(1.0),
+
+        "A = 0.0e23",
+        expectTrue(0.0), expectFalse(1.0),
+
+        "A = 1.2e3",
+        expectTrue(1.2e3), expectFalse(1.2),
+
+        "A = +1.2",
+        expectTrue(1.2), expectFalse(-1.2),
+
+        "A = 1.2e+3",
+        expectTrue(1.2e3), expectFalse(1.2),
+
+        "A = 1.2e-3",
+        expectTrue(1.2e-3), expectFalse(1.2),
+
+        "A = 1.2E3",
+        expectTrue(1.2e3), expectFalse(1.2),
+
+        "A = -1.2e3",
+        expectTrue(-1.2e3), expectFalse(1.2),
+
+        "A = " + Double.MAX_VALUE,
+        expectTrue(Double.MAX_VALUE), expectFalse(Double.MIN_VALUE),
+
+        "A = " + -Double.MAX_VALUE,
+        expectTrue(-Double.MAX_VALUE), expectFalse(-Double.MIN_VALUE),
+
+        "A = " + Double.MIN_VALUE,
+        expectTrue(Double.MIN_VALUE), expectFalse(Double.MAX_VALUE),
+
+        "A = " + -Double.MIN_VALUE,
+        expectTrue(-Double.MIN_VALUE), expectFalse(-Double.MAX_VALUE),
+
+        Query.toString(  // A = Infinity   ->   A = (1.0/0.0)
+                Query.eq(Query.attr("A"), Query.value(Double.POSITIVE_INFINITY))),
+        expectTrue(Double.POSITIVE_INFINITY),
+        expectFalse(0.0), expectFalse(Double.NEGATIVE_INFINITY),
+
+        Query.toString(  // A = -Infinity   ->   A = (-1.0/0.0)
+                Query.eq(Query.attr("A"), Query.value(Double.NEGATIVE_INFINITY))),
+        expectTrue(Double.NEGATIVE_INFINITY),
+        expectFalse(0.0), expectFalse(Double.POSITIVE_INFINITY),
+
+        Query.toString(  // A < NaN   ->   A < (0.0/0.0)
+                Query.lt(Query.attr("A"), Query.value(Double.NaN))),
+        expectFalse(0.0), expectFalse(Double.NEGATIVE_INFINITY),
+        expectFalse(Double.POSITIVE_INFINITY), expectFalse(Double.NaN),
+
+        Query.toString(  // A >= NaN   ->   A < (0.0/0.0)
+                Query.geq(Query.attr("A"), Query.value(Double.NaN))),
+        expectFalse(0.0), expectFalse(Double.NEGATIVE_INFINITY),
+        expectFalse(Double.POSITIVE_INFINITY), expectFalse(Double.NaN),
+
+        // STRING LITERALS
+
+        "A = 'blim'",
+        expectTrue("blim"), expectFalse("blam"),
+
+        "A = 'can''t'",
+        expectTrue("can't"), expectFalse("cant"), expectFalse("can''t"),
+
+        "A = '''blim'''",
+        expectTrue("'blim'"), expectFalse("'blam'"),
+
+        "A = ''",
+        expectTrue(""), expectFalse((Object) null),
+
+        // BOOLEAN LITERALS
+
+        "A = true",
+        expectTrue(true), expectFalse(false), expectFalse((Object) null),
+
+        "A = TRUE",
+        expectTrue(true), expectFalse(false),
+
+        "A = TrUe",
+        expectTrue(true), expectFalse(false),
+
+        "A = false",
+        expectTrue(false), expectFalse(true),
+
+        "A = fAlSe",
+        expectTrue(false), expectFalse(true),
+
+        "A = \"true\"",   // An attribute called "true"
+        expectFalse(true), expectFalse(false), expectFalse("\"true\""),
+        newTester(new String[] {"A", "true"}, new Object[] {2.2, 2.2}, true),
+        newTester(new String[] {"A", "true"}, new Object[] {2.2, 2.3}, false),
+
+        "A = \"False\"",
+        expectFalse(true), expectFalse(false), expectFalse("\"False\""),
+        newTester(new String[] {"A", "False"}, new Object[] {2.2, 2.2}, true),
+        newTester(new String[] {"A", "False"}, new Object[] {2.2, 2.3}, false),
+
+        // ARITHMETIC
+
+        "A + B = 10",
+        expectTrue(4, 6), expectFalse(3, 8),
+
+        "A + B = 'blim'",
+        expectTrue("bl", "im"), expectFalse("bl", "am"),
+
+        "A - B = 10",
+        expectTrue(16, 6), expectFalse(16, 3),
+
+        "A * B = 10",
+        expectTrue(2, 5), expectFalse(3, 3),
+
+        "A / B = 10",
+        expectTrue(70, 7), expectTrue(70.0, 7), expectFalse(70.01, 7),
+
+        "A + B + C = 10",
+        expectTrue(2, 3, 5), expectFalse(2, 4, 8),
+
+        "A+B+C=10",
+        expectTrue(2, 3, 5), expectFalse(2, 4, 8),
+
+        "A + B + C + D = 10",
+        expectTrue(1, 2, 3, 4), expectFalse(2, 3, 4, 5),
+
+        "A + (B + C) = 10",
+        expectTrue(2, 3, 5), expectFalse(2, 4, 8),
+
+        // It is not correct to rearrange A + (B + C) as A + B + C
+        // (which means (A + B) + C), because of overflow.
+        // In particular Query.toString must not do this.
+        "A + (B + C) = " + Double.MAX_VALUE,  // ensure no false associativity
+        expectTrue(Double.MAX_VALUE, Double.MAX_VALUE, -Double.MAX_VALUE),
+        expectFalse(-Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE),
+
+        "A * (B * C) < " + Double.MAX_VALUE,  // same test for multiplication
+        expectTrue(Double.MAX_VALUE, Double.MAX_VALUE, Double.MIN_VALUE),
+        expectFalse(Double.MIN_VALUE, Double.MAX_VALUE, Double.MAX_VALUE),
+
+        "A * B + C = 10",
+        expectTrue(3, 3, 1), expectTrue(2, 4, 2), expectFalse(1, 2, 3),
+
+        "A*B+C=10",
+        expectTrue(3, 3, 1), expectTrue(2, 4, 2), expectFalse(1, 2, 3),
+
+        "(A * B) + C = 10",
+        expectTrue(3, 3, 1), expectTrue(2, 4, 2), expectFalse(1, 2, 3),
+
+        "A + B * C = 10",
+        expectTrue(1, 3, 3), expectTrue(2, 2, 4), expectFalse(1, 2, 3),
+
+        "A - B * C = 10",
+        expectTrue(16, 2, 3), expectFalse(15, 2, 2),
+
+        "A + B / C = 10",
+        expectTrue(5, 15, 3), expectFalse(5, 16, 4),
+
+        "A - B / C = 10",
+        expectTrue(16, 12, 2), expectFalse(15, 10, 3),
+
+        "A * (B + C) = 10",
+        expectTrue(2, 2, 3), expectFalse(1, 2, 3),
+
+        "A / (B + C) = 10",
+        expectTrue(70, 4, 3), expectFalse(70, 3, 5),
+
+        "A * (B - C) = 10",
+        expectTrue(2, 8, 3), expectFalse(2, 3, 8),
+
+        "A / (B - C) = 10",
+        expectTrue(70, 11, 4), expectFalse(70, 4, 11),
+
+        "A / B / C = 10",
+        expectTrue(140, 2, 7), expectFalse(100, 5, 5),
+
+        "A / (B / C) = 10",
+        expectTrue(70, 14, 2), expectFalse(70, 10, 7),
+
+        // LOGIC
+
+        "A = B or C = D",
+        expectTrue(1, 1, 2, 3), expectTrue(1, 2, 3, 3), expectTrue(1, 1, 2, 2),
+        expectFalse(1, 2, 3, 4), expectFalse("!", "!!", "?", "??"),
+
+        "A = B and C = D",
+        expectTrue(1, 1, 2, 2),
+        expectFalse(1, 1, 2, 3), expectFalse(1, 2, 3, 3),
+
+        "A = 1 and B = 2 and C = 3",
+        expectTrue(1, 2, 3), expectFalse(1, 2, 4),
+
+        "A = 1 or B = 2 or C = 3",
+        expectTrue(1, 2, 3), expectTrue(1, 0, 0), expectTrue(0, 0, 3),
+        expectFalse(2, 3, 4),
+
+        // grouped as (a and b) or (c and d)
+        "A = 1 AND B = 2 OR C = 3 AND D = 4",
+        expectTrue(1, 2, 3, 4), expectTrue(1, 2, 1, 2), expectTrue(3, 4, 3, 4),
+        expectFalse(3, 4, 1, 2), expectFalse(1, 1, 1, 1),
+
+        "(A = 1 AND B = 2) OR (C = 3 AND D = 4)",
+        expectTrue(1, 2, 3, 4), expectTrue(1, 2, 1, 2), expectTrue(3, 4, 3, 4),
+        expectFalse(3, 4, 1, 2), expectFalse(1, 1, 1, 1),
+
+        "(A = 1 or B = 2) AND (C = 3 or C = 4)",
+        expectTrue(1, 1, 3, 3), expectTrue(2, 2, 4, 4), expectTrue(1, 2, 3, 4),
+        expectFalse(1, 2, 1, 2), expectFalse(3, 4, 3, 4),
+
+        // LIKE
+
+        "A like 'b%m'",
+        expectTrue("blim"), expectTrue("bm"),
+        expectFalse(""), expectFalse("blimmo"), expectFalse("mmm"),
+
+        "A not like 'b%m'",
+        expectFalse("blim"), expectFalse("bm"),
+        expectTrue(""), expectTrue("blimmo"), expectTrue("mmm"),
+
+        "A like 'b_m'",
+        expectTrue("bim"), expectFalse("blim"),
+
+        "A like '%can''t%'",
+        expectTrue("can't"),
+        expectTrue("I'm sorry Dave, I'm afraid I can't do that"),
+        expectFalse("cant"), expectFalse("can''t"),
+
+        "A like '\\%%\\%'",
+        expectTrue("%blim%"), expectTrue("%%"),
+        expectFalse("blim"), expectFalse("%asdf"), expectFalse("asdf%"),
+
+        "A LIKE '*%?_'",
+        expectTrue("*blim?!"), expectTrue("*?_"),
+        expectFalse("blim"), expectFalse("blim?"),
+        expectFalse("?*"), expectFalse("??"), expectFalse(""), expectFalse("?"),
+
+        Query.toString(
+                Query.initialSubString(Query.attr("A"), Query.value("*?%_"))),
+        expectTrue("*?%_tiddly"), expectTrue("*?%_"),
+        expectFalse("?%_tiddly"), expectFalse("*!%_"), expectFalse("*??_"),
+        expectFalse("*?%!"), expectFalse("*?%!tiddly"),
+
+        Query.toString(
+                Query.finalSubString(Query.attr("A"), Query.value("*?%_"))),
+        expectTrue("tiddly*?%_"), expectTrue("*?%_"),
+        expectFalse("tiddly?%_"), expectFalse("*!%_"), expectFalse("*??_"),
+        expectFalse("*?%!"), expectFalse("tiddly*?%!"),
+
+        // BETWEEN
+
+        "A between B and C",
+        expectTrue(1, 1, 2), expectTrue(2, 1, 2), expectTrue(2, 1, 3),
+        expectFalse(3, 1, 2), expectFalse(0, 1, 2), expectFalse(2, 3, 1),
+        expectTrue(1.0, 0.0, 2.0), expectFalse(2.0, 0.0, 1.0),
+        expectTrue(0.0, 0.0, 0.0), expectTrue(1.0, 0.0, 1.0),
+        expectTrue(1.0, 0.0, Double.POSITIVE_INFINITY),
+        expectFalse(1.0, Double.NEGATIVE_INFINITY, 0.0),
+        expectFalse(false, false, true), expectFalse(true, false, true),
+        expectTrue("jim", "fred", "sheila"), expectFalse("fred", "jim", "sheila"),
+
+        "A between B and C and 1+2=3",
+        expectTrue(2, 1, 3), expectFalse(2, 3, 1),
+
+        "A not between B and C",
+        expectTrue(1, 2, 3), expectFalse(2, 1, 3),
+
+        // IN
+
+        "A in (1, 2, 3)",
+        expectTrue(1), expectTrue(2), expectTrue(3),
+        expectFalse(0), expectFalse(4),
+
+        "A in (1)",
+        expectTrue(1), expectFalse(0),
+
+        "A in (1.2, 3.4)",
+        expectTrue(1.2), expectTrue(3.4), expectFalse(0.0),
+
+        "A in ('foo', 'bar')",
+        expectTrue("foo"), expectTrue("bar"), expectFalse("baz"),
+
+        "A in ('foo', 'bar') and 'bl'+'im'='blim'",
+        expectTrue("foo"), expectTrue("bar"), expectFalse("baz"),
+
+        "A in (B, C, D)",  // requires fix for CR 6604768
+        expectTrue(1, 1, 2, 3), expectFalse(1, 2, 3, 4),
+
+        "A not in (B, C, D)",
+        expectTrue(1, 2, 3, 4), expectFalse(1, 1, 2, 3),
+
+        // QUOTING
+
+        "\"LIKE\" = 1 and \"NOT\" = 2 and \"INSTANCEOF\" = 3 and " +
+                "\"TRUE\" = 4 and \"FALSE\" = 5",
+        newTester(
+                new String[] {"LIKE", "NOT", "INSTANCEOF", "TRUE", "FALSE"},
+                new Object[] {1, 2, 3, 4, 5},
+                true),
+        newTester(
+                new String[] {"LIKE", "NOT", "INSTANCEOF", "TRUE", "FALSE"},
+                new Object[] {5, 4, 3, 2, 1},
+                false),
+
+        "\"\"\"woo\"\"\" = 5",
+        newTester(new String[] {"\"woo\""}, new Object[] {5}, true),
+        newTester(new String[] {"\"woo\""}, new Object[] {4}, false),
+        expectFalse(),
+
+        // INSTANCEOF
+
+        "instanceof '" + Tester.class.getName() + "'",
+        expectTrue(),
+
+        "instanceof '" + String.class.getName() + "'",
+        expectFalse(),
+
+        // LIKE OBJECTNAME
+
+        // The test MBean is registered as a:b=c
+        "like 'a:b=c'", expectTrue(),
+        "like 'a:*'", expectTrue(),
+        "like '*:b=c'", expectTrue(),
+        "like 'a:b=*'", expectTrue(),
+        "like 'a:b=?'", expectTrue(),
+        "like 'd:b=c'", expectFalse(),
+        "like 'a:b=??*'", expectFalse(),
+        "like 'a:b=\"can''t\"'", expectFalse(),
+
+        // QUALIFIED ATTRIBUTE
+
+        Tester.class.getName() + "#A = 5",
+        expectTrue(5), expectFalse(4),
+
+        Tester.class.getName() + " # A = 5",
+        expectTrue(5), expectFalse(4),
+
+        Tester.class.getSuperclass().getName() + "#A = 5",
+        expectFalse(5),
+
+        DynamicMBean.class.getName() + "#A = 5",
+        expectFalse(5),
+
+        Tester.class.getName() + "#A = 5",
+        new Tester(new String[] {"A"}, new Object[] {5}, false) {},
+        // note the little {} at the end which means this is a subclass
+        // and therefore QualifiedAttributeValue should return false.
+
+        MBeanServerDelegate.class.getName() + "#SpecificationName LIKE '%'",
+        new Wrapped(new MBeanServerDelegate(), true),
+        new Tester(new String[] {"SpecificationName"}, new Object[] {"JMX"}, false),
+
+        // DOTTED ATTRIBUTE
+
+        "A.canonicalName = '" +
+                MBeanServerDelegate.DELEGATE_NAME.getCanonicalName() + "'",
+        expectTrue(MBeanServerDelegate.DELEGATE_NAME),
+        expectFalse(ObjectName.WILDCARD),
+
+        "A.class.name = 'java.lang.String'",
+        expectTrue("blim"), expectFalse(95), expectFalse((Object) null),
+
+        "A.canonicalName like 'JMImpl%:%'",
+        expectTrue(MBeanServerDelegate.DELEGATE_NAME),
+        expectFalse(ObjectName.WILDCARD),
+
+        "A.true = 'blim'",
+        new Tester(new String[] {"A.true"}, new Object[] {"blim"}, true),
+        new Tester(new String[] {"A.true"}, new Object[] {"blam"}, false),
+
+        "\"A.true\" = 'blim'",
+        new Tester(new String[] {"A.true"}, new Object[] {"blim"}, true),
+        new Tester(new String[] {"A.true"}, new Object[] {"blam"}, false),
+
+        MBeanServerDelegate.class.getName() +
+                "#SpecificationName.class.name = 'java.lang.String'",
+        new Wrapped(new MBeanServerDelegate(), true),
+        new Tester(new String[] {"SpecificationName"}, new Object[] {"JMX"}, false),
+
+        MBeanServerDelegate.class.getName() +
+                " # SpecificationName.class.name = 'java.lang.String'",
+        new Wrapped(new MBeanServerDelegate(), true),
+        new Tester(new String[] {"SpecificationName"}, new Object[] {"JMX"}, false),
+
+        // CLASS
+
+        "class = '" + Tester.class.getName() + "'",
+        expectTrue(),
+        new Wrapped(new MBeanServerDelegate(), false),
+
+        "Class = '" + Tester.class.getName() + "'",
+        expectTrue(),
+        new Wrapped(new MBeanServerDelegate(), false),
+    };
+
+    private static final String[] incorrectQueries = {
+        "", " ", "25", "()", "(a = b", "a = b)", "a.3 = 5",
+        "a = " + Long.MAX_VALUE + "0",
+        "a = " + Double.MAX_VALUE + "0",
+        "a = " + Double.MIN_VALUE + "0",
+        "a = 12a5", "a = 12e5e5", "a = 12.23.34",
+        "a = 'can't'", "a = 'unterminated", "a = 'asdf''",
+        "a = \"oops", "a = \"oops\"\"",
+        "a like 5", "true or false",
+        "a ! b", "? = 3", "a = @", "a##b",
+        "a between b , c", "a between and c",
+        "a in b, c", "a in 23", "a in (2, 3", "a in (2, 3x)",
+        "a like \"foo\"", "a like b", "a like 23",
+        "like \"foo\"", "like b", "like 23", "like 'a:b'",
+        "5 like 'a'", "'a' like '%'",
+        "a not= b", "a not = b", "a not b", "a not b c",
+        "a = +b", "a = +'b'", "a = +true", "a = -b", "a = -'b'",
+        "a#5 = b", "a#'b' = c",
+        "a instanceof b", "a instanceof 17", "a instanceof",
+        "a like 'oops\\'", "a like '[oops'",
+
+        // Check that -Long.MIN_VALUE is an illegal constant.  This is one more
+        // than Long.MAX_VALUE and, like the Java language, we only allow it
+        // if it is the operand of unary minus.
+        "a = " + Long.toString(Long.MIN_VALUE).substring(1),
+    };
+
+    public static void main(String[] args) throws Exception {
+        int nexti;
+        String failed = null;
+
+        System.out.println("TESTING CORRECT QUERY STRINGS");
+        for (int i = 0; i < queryTests.length; i = nexti) {
+            for (nexti = i + 1; nexti < queryTests.length; nexti++) {
+                if (queryTests[nexti] instanceof String)
+                    break;
+            }
+            if (!(queryTests[i] instanceof String))
+                throw new Exception("Test bug: should be string: " + queryTests[i]);
+
+            String qs = (String) queryTests[i];
+            System.out.println("Test: " + qs);
+
+            QueryExp qe = Query.fromString(qs);
+            String qes = Query.toString(qe);
+            System.out.println("...parses to: " + qes);
+            final QueryExp[] queries;
+            if (qes.equals(qs))
+                queries = new QueryExp[] {qe};
+            else {
+                QueryExp qe2 = Query.fromString(qes);
+                String qes2 = Query.toString(qe2);
+                System.out.println("...which parses to: " + qes2);
+                if (qes.equals(qes2))
+                    queries = new QueryExp[] {qe};
+                else
+                    queries = new QueryExp[] {qe, qe2};
+            }
+
+            for (int j = i + 1; j < nexti; j++) {
+                Object mbean;
+                if (queryTests[j] instanceof Wrapped)
+                    mbean = ((Wrapped) queryTests[j]).mbean();
+                else
+                    mbean = queryTests[j];
+                boolean expect = ((ExpectedValue) queryTests[j]).expectedValue();
+                for (QueryExp qet : queries) {
+                    boolean actual = runQuery(qet, mbean);
+                    boolean ok = (expect == actual);
+                    System.out.println(
+                            "..." + mbean + " -> " + actual +
+                            (ok ? " (OK)" : " ####INCORRECT####"));
+                    if (!ok)
+                        failed = qs;
+                }
+            }
+        }
+
+        System.out.println();
+        System.out.println("TESTING INCORRECT QUERY STRINGS");
+        for (String s : incorrectQueries) {
+            try {
+                QueryExp qe = Query.fromString(s);
+                System.out.println("###DID NOT GET ERROR:### \"" + s + "\"");
+                failed = s;
+            } catch (IllegalArgumentException e) {
+                String es = (e.getClass() == IllegalArgumentException.class) ?
+                    e.getMessage() : e.toString();
+                System.out.println("OK: exception for \"" + s + "\": " + es);
+            }
+        }
+
+        if (failed == null)
+            System.out.println("TEST PASSED");
+        else
+            throw new Exception("TEST FAILED: Last failure: " + failed);
+    }
+
+    private static boolean runQuery(QueryExp qe, Object mbean)
+    throws Exception {
+        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+        ObjectName name = new ObjectName("a:b=c");
+        mbs.registerMBean(mbean, name);
+        Set<ObjectName> names = mbs.queryNames(new ObjectName("a:*"), qe);
+        if (names.isEmpty())
+            return false;
+        if (names.equals(Collections.singleton(name)))
+            return true;
+        throw new Exception("Unexpected query result set: " + names);
+    }
+
+    private static interface ExpectedValue {
+        public boolean expectedValue();
+    }
+
+    private static class Wrapped implements ExpectedValue {
+        private final Object mbean;
+        private final boolean expect;
+
+        Wrapped(Object mbean, boolean expect) {
+            this.mbean = mbean;
+            this.expect = expect;
+        }
+
+        Object mbean() {
+            return mbean;
+        }
+
+        public boolean expectedValue() {
+            return expect;
+        }
+    }
+
+    private static class Tester implements DynamicMBean, ExpectedValue {
+        private final AttributeList attributes;
+        private final boolean expectedValue;
+
+        Tester(AttributeList attributes, boolean expectedValue) {
+            this.attributes = attributes;
+            this.expectedValue = expectedValue;
+        }
+
+        Tester(String[] names, Object[] values, boolean expectedValue) {
+            this(makeAttributeList(names, values), expectedValue);
+        }
+
+        private static AttributeList makeAttributeList(
+                String[] names, Object[] values) {
+            if (names.length != values.length)
+                throw new Error("Test bug: names and values different length");
+            AttributeList list = new AttributeList();
+            for (int i = 0; i < names.length; i++)
+                list.add(new Attribute(names[i], values[i]));
+            return list;
+        }
+
+        public Object getAttribute(String attribute)
+        throws AttributeNotFoundException {
+            for (Attribute a : attributes.asList()) {
+                if (a.getName().equals(attribute))
+                    return a.getValue();
+            }
+            throw new AttributeNotFoundException(attribute);
+        }
+
+        public void setAttribute(Attribute attribute) {
+            throw new UnsupportedOperationException();
+        }
+
+        public AttributeList getAttributes(String[] attributes) {
+            AttributeList list = new AttributeList();
+            for (String attribute : attributes) {
+                try {
+                    list.add(new Attribute(attribute, getAttribute(attribute)));
+                } catch (AttributeNotFoundException e) {
+                    // OK: ignore, per semantics of getAttributes
+                }
+            }
+            return list;
+        }
+
+        public AttributeList setAttributes(AttributeList attributes) {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object invoke(String actionName, Object[] params, String[] signature) {
+            throw new UnsupportedOperationException();
+        }
+
+        public MBeanInfo getMBeanInfo() {
+            MBeanAttributeInfo mbais[] = new MBeanAttributeInfo[attributes.size()];
+            for (int i = 0; i < mbais.length; i++) {
+                Attribute attr = attributes.asList().get(i);
+                String name = attr.getName();
+                Object value = attr.getValue();
+                String type =
+                        ((value == null) ? new Object() : value).getClass().getName();
+                mbais[i] = new MBeanAttributeInfo(
+                        name, type, name, true, false, false);
+            }
+            return new MBeanInfo(
+                    getClass().getName(), "descr", mbais, null, null, null);
+        }
+
+        public boolean expectedValue() {
+            return expectedValue;
+        }
+
+        @Override
+        public String toString() {
+            return attributes.toString();
+        }
+    }
+
+    // Method rather than field, to avoid circular init dependencies
+    private static String[] abcd() {
+        return new String[] {"A", "B", "C", "D"};
+    }
+
+    private static String[] onetwo() {
+        return new String[] {"One", "two"};
+    }
+
+    private static Object expectTrue(Object... attrs) {
+        return newTester(abcd(), attrs, true);
+    }
+
+    private static Object expectFalse(Object... attrs) {
+        return newTester(abcd(), attrs, false);
+    }
+
+    private static Object expectTrueOneTwo(Object... attrs) {
+        return newTester(onetwo(), attrs, true);
+    }
+
+    private static Object expectFalseOneTwo(Object... attrs) {
+        return newTester(onetwo(), attrs, false);
+    }
+
+    private static Object newTester(String[] names, Object[] attrs, boolean expect) {
+        AttributeList list = new AttributeList();
+        for (int i = 0; i < attrs.length; i++)
+            list.add(new Attribute(names[i], attrs[i]));
+        return new Tester(list, expect);
+    }
+}