(FELIX-1473) use alternative implementation for method calls on Strings


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@959276 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
index 72cb58b..c93036e 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
@@ -222,6 +222,28 @@
         return last == null ? null : last.result;
     }
 
+    private Object eval(Object v)
+    {
+        String s = v.toString();
+        if ("null".equals(s))
+        {
+            v = null;
+        }
+        else if ("false".equals(s))
+        {
+            v = false;
+        }
+        else if ("true".equals(s))
+        {
+            v = true;
+        }
+        else
+        {
+            v = s;
+        }
+        return v;
+    }
+
     public Object eval(final Token t) throws Exception
     {
         Object v = null;
@@ -230,26 +252,10 @@
         {
             case WORD:
                 v = Tokenizer.expand(t, this);
-                
+
                 if (t == v)
                 {
-                    String s = v.toString();
-                    if ("null".equals(s))
-                    {
-                        v = null;
-                    }
-                    else if ("false".equals(s))
-                    {
-                        v = false;
-                    }
-                    else if ("true".equals(s))
-                    {
-                        v = true;
-                    }
-                    else
-                    {
-                        v = s;
-                    }
+                    v = eval(v);
                 }
                 else if (v instanceof CharSequence)
                 {
@@ -280,18 +286,30 @@
         return v;
     }
 
+    /*
+     * executeStatement handles the following cases:
+     *    <string> '=' word // simple assignment
+     *    <string> '=' word word.. // complex assignment
+     *    <bareword> word.. // command invocation
+     *    <object> // value of <object>
+     *    <object> word.. // method call
+     */
     public Object executeStatement(List<Token> statement) throws Exception
     {
-        // add set -x facility if echo is set
-        if (Boolean.TRUE.equals(session.get("echo")))
+        Object echo = session.get("echo");
+        String xtrace = null;
+        
+        if (echo != null && !"false".equals(echo.toString()))
         {
+            // set -x execution trace
             StringBuilder buf = new StringBuilder("+");
             for (Token token : statement)
             {
                 buf.append(' ');
                 buf.append(token.source());
             }
-            session.err.println(buf);
+            xtrace = buf.toString();
+            session.err.println(xtrace);
         }
 
         List<Object> values = new ArrayList<Object>();
@@ -332,138 +350,167 @@
             throw new RuntimeException("Command name evaluates to null: " + errTok);
         }
 
-        return execute(cmd, values);
+        if (cmd instanceof CharSequence && values.size() > 0
+            && Type.ASSIGN.equals(values.get(0)))
+        {
+            values.remove(0);
+            String scmd = cmd.toString();
+            Object value;
+
+            if (values.size() == 0)
+            {
+                return session.variables.remove(scmd);
+            }
+
+            if (values.size() == 1)
+            {
+                value = values.get(0);
+            }
+            else
+            {
+                cmd = values.remove(0);
+                if (null == cmd)
+                {
+                    throw new RuntimeException("Command name evaluates to null: "
+                        + errTok2);
+                }
+
+                trace2(xtrace, cmd, values);
+
+                value = bareword(statement.get(2)) ? executeCmd(cmd.toString(), values)
+                    : executeMethod(cmd, values);
+            }
+
+            return assignment(scmd, value);
+        }
+
+        trace2(xtrace, cmd, values);
+
+        return bareword(statement.get(0)) ? executeCmd(cmd.toString(), values)
+            : executeMethod(cmd, values);
     }
 
-    private Object execute(Object cmd, List<Object> values) throws Exception
+    // second level expanded execution trace
+    private void trace2(String trace1, Object cmd, List<Object> values)
     {
-        // Now there are the following cases
-        // <string> '=' statement // complex assignment
-        // <string> statement // cmd call
-        // <object> // value of <object>
-        // <object> statement // method call
+        if ("verbose".equals(session.get("echo")))
+        {
+            StringBuilder buf = new StringBuilder("+ " + cmd);
+
+            for (Object value : values)
+            {
+                buf.append(' ');
+                buf.append(value);
+            }
+
+            String trace2 = buf.toString();
+
+            if (!trace2.equals(trace1))
+            {
+                session.err.println("+" + trace2);
+            }
+        }
+    }
+
+    private boolean bareword(Token t) throws Exception
+    {
+        return ((t.type == Type.WORD) && (t == Tokenizer.expand(t, this)) && (eval((Object) t) instanceof String));
+    }
+
+    private Object executeCmd(String scmd, List<Object> values) throws Exception
+    {
+        String scopedFunction = scmd;
+        Object x = get(scmd);
+
+        if (!(x instanceof Function))
+        {
+            if (scmd.indexOf(':') < 0)
+            {
+                scopedFunction = "*:" + scmd;
+            }
+
+            x = get(scopedFunction);
+
+            if (x == null || !(x instanceof Function))
+            {
+                // try default command handler
+                if (session.get(DEFAULT_LOCK) == null)
+                {
+                    x = get("default");
+                    if (x == null)
+                    {
+                        x = get("*:default");
+                    }
+
+                    if (x instanceof Function)
+                    {
+                        try
+                        {
+                            session.put(DEFAULT_LOCK, true);
+                            values.add(0, scmd);
+                            return ((Function) x).execute(session, values);
+                        }
+                        finally
+                        {
+                            session.variables.remove(DEFAULT_LOCK);
+                        }
+                    }
+                }
+
+                throw new IllegalArgumentException("Command not found: " + scmd);
+            }
+        }
+        return ((Function) x).execute(session, values);
+    }
+
+    private Object executeMethod(Object cmd, List<Object> values) throws Exception
+    {
+        if (values.isEmpty())
+        {
+            return cmd;
+        }
 
         boolean dot = values.size() > 1 && ".".equals(String.valueOf(values.get(0)));
 
-        if (cmd instanceof CharSequence && !dot)
+        // FELIX-1473 - allow method chaining using dot pseudo-operator, e.g.
+        //  (bundle 0) . loadClass java.net.InetAddress . localhost . hostname
+        //  (((bundle 0) loadClass java.net.InetAddress ) localhost ) hostname
+        if (dot)
         {
-            String scmd = cmd.toString();
+            Object target = cmd;
+            ArrayList<Object> args = new ArrayList<Object>();
+            values.remove(0);
 
-            if (values.size() > 0 && Type.ASSIGN.equals(values.get(0)))
+            for (Object arg : values)
             {
-                Object value;
-
-                if (values.size() == 1)
+                if (".".equals(arg))
                 {
-                    return session.variables.remove(scmd);
-                }
-
-                if (values.size() == 2)
-                {
-                    value = values.get(1);
+                    target = Reflective.method(session, target,
+                        args.remove(0).toString(), args);
+                    args.clear();
                 }
                 else
                 {
-                    cmd = values.get(1);
-                    if (null == cmd)
-                    {
-                        throw new RuntimeException("Command name evaluates to null: "
-                            + errTok2);
-                    }
-                    value = execute(cmd, values.subList(2, values.size()));
+                    args.add(arg);
                 }
-
-                return assignment(scmd, value);
             }
-            else
+
+            if (args.size() == 0)
             {
-                String scopedFunction = scmd;
-                Object x = get(scmd);
-
-                if (!(x instanceof Function))
-                {
-                    if (scmd.indexOf(':') < 0)
-                    {
-                        scopedFunction = "*:" + scmd;
-                    }
-
-                    x = get(scopedFunction);
-
-                    if (x == null || !(x instanceof Function))
-                    {
-                        // try default command handler
-                        if (session.get(DEFAULT_LOCK) == null)
-                        {
-                            x = get("default");
-                            if (x == null)
-                            {
-                                x = get("*:default");
-                            }
-
-                            if (x instanceof Function)
-                            {
-                                try
-                                {
-                                    session.put(DEFAULT_LOCK, true);
-                                    values.add(0, scmd);
-                                    return ((Function) x).execute(session, values);
-                                }
-                                finally
-                                {
-                                    session.variables.remove(DEFAULT_LOCK);
-                                }
-                            }
-                        }
-
-                        throw new IllegalArgumentException("Command not found: " + scmd);
-                    }
-                }
-                return ((Function) x).execute(session, values);
+                return target;
             }
+
+            return Reflective.method(session, target, args.remove(0).toString(), args);
+        }
+        else if (cmd.getClass().isArray() && values.size() == 1)
+        {
+            Object[] cmdv = (Object[]) cmd;
+            String index = values.get(0).toString();
+            return "length".equals(index) ? cmdv.length : cmdv[Integer.parseInt(index)];
         }
         else
         {
-            if (values.isEmpty())
-            {
-                return cmd;
-            }
-            else if (dot)
-            {
-                // FELIX-1473 - allow methods calls on String objects
-                Object target = cmd;
-                ArrayList<Object> args = new ArrayList<Object>();
-                values.remove(0);
-
-                for (Object arg : values)
-                {
-                    if (".".equals(arg))
-                    {
-                        target = Reflective.method(session, target, args.remove(0).toString(), args);
-                        args.clear();
-                    }
-                    else
-                    {
-                        args.add(arg);
-                    }
-                }
-
-                if (args.size() == 0)
-                {
-                    return target;
-                }
-
-                return Reflective.method(session, target, args.remove(0).toString(), args);
-            }
-            else if (cmd.getClass().isArray() && values.size() == 1)
-            {
-                Object[] cmdv = (Object[])cmd;
-                String index = values.get(0).toString();
-                return "length".equals(index) ? cmdv.length : cmdv[Integer.parseInt(index)];
-            }
-            else
-            {
-                return Reflective.method(session, cmd, values.remove(0).toString(), values);
-            }
+            return Reflective.method(session, cmd, values.remove(0).toString(), values);
         }
     }
 
@@ -487,7 +534,7 @@
                 Object oval = eval(t);
                 if (oval.getClass().isArray())
                 {
-                    for (Object o : (Object[])oval)
+                    for (Object o : (Object[]) oval)
                     {
                         olist.add(o);
                     }
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
index ca0e9cf..b79ba01 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
@@ -49,6 +49,7 @@
         this.context = context;
         addCommand("osgi", this, "addCommand");
         addCommand("osgi", this, "removeCommand");
+        addCommand("osgi", this, "eval");
     }
 
     public CommandSession createSession(InputStream in, PrintStream out, PrintStream err)
@@ -65,12 +66,12 @@
     {
         converters.remove(c);
     }
-    
+
     public Set<String> getCommands()
     {
         return commands.keySet();
     }
-    
+
     BundleContext getContext()
     {
         return context;
@@ -84,16 +85,16 @@
         {
             return null;
         }
-        
+
         name = name.toLowerCase();
         Object cmd = commands.get(name);
         String cfunction = name.substring(colon);
         boolean anyScope = (colon == 1 && name.charAt(0) == '*');
-        
+
         if (null == cmd && anyScope)
         {
             String scopePath = (null == path ? "*" : path.toString());
-            
+
             for (String scope : scopePath.split(":"))
             {
                 if (scope.equals("*"))
@@ -111,7 +112,7 @@
                 {
                     cmd = commands.get(scope + cfunction);
                 }
-                
+
                 if (cmd != null)
                 {
                     break;
@@ -220,4 +221,19 @@
         }
         return null;
     }
+
+    // eval is needed to force expansions to be treated as commands (FELIX-1473)
+    public Object eval(CommandSession session, Object[] argv) throws Exception
+    {
+        StringBuilder buf = new StringBuilder();
+
+        for (Object arg : argv)
+        {
+            if (buf.length() > 0)
+                buf.append(' ');
+            buf.append(arg);
+        }
+
+        return session.execute(buf);
+    }
 }
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java
index e47d0a7..9d26323 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java
@@ -154,7 +154,7 @@
 
         assertEquals("a", c.execute("e = { echo $1 } ; e a   b"));
         assertEquals("b", c.execute("e = { echo $2 } ; e a   b"));
-        assertEquals("b", c.execute("e = { $args } ; e echo  b"));
+        assertEquals("b", c.execute("e = { eval $args } ; e echo  b"));
         assertEquals("ca b", c.execute("e = { echo c$args } ; e a  b"));
         assertEquals("c a b", c.execute("e = { echo c $args } ; e a  b"));
         assertEquals("ca  b", c.execute("e = { echo c$args } ; e 'a  b'"));
@@ -233,8 +233,8 @@
         Context c = new Context();
         c.addCommand("echo", this);
         assertEquals("a", c.execute("echo a") + "");
-        assertEquals("a", c.execute("(echo echo) a") + "");
-        assertEquals("a", c.execute("((echo echo) echo) (echo a)") + "");
+        assertEquals("a", c.execute("eval (echo echo) a") + "");
+        //assertEquals("a", c.execute("((echo echo) echo) (echo a)") + "");
         assertEquals("3", c.execute("[a=2 (echo b)=(echo 3)] get b").toString());
     }
 
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java
index 485c988..2ef58ff 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java
@@ -69,6 +69,15 @@
         c.addCommand("echo", this);
         // FELIX-2433
         assertEquals("helloworld", c.execute("echo \"$(echo hello)world\""));
+        
+         // FELIX-1473 - allow method calls on String objects
+        assertEquals("hello", c.execute("cmd = echo; eval $cmd hello"));
+        assertEquals(4, c.execute("'four' length"));
+        try {
+            c.execute("four length");
+            fail("expected: command not found: four");
+        } catch (IllegalArgumentException e) {
+        }
     }
 
     public CharSequence echo(Object args[])