| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.felix.gogo.runtime; |
| |
| import java.io.*; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.Channel; |
| import java.nio.channels.Channels; |
| import java.nio.channels.ClosedChannelException; |
| import java.nio.channels.WritableByteChannel; |
| import java.nio.channels.spi.AbstractInterruptibleChannel; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import org.apache.felix.service.command.Job.Status; |
| import org.apache.felix.gogo.runtime.Parser.Array; |
| import org.apache.felix.gogo.runtime.Parser.Executable; |
| import org.apache.felix.gogo.runtime.Parser.Operator; |
| import org.apache.felix.gogo.runtime.Parser.Pipeline; |
| import org.apache.felix.gogo.runtime.Parser.Program; |
| import org.apache.felix.gogo.runtime.Parser.Sequence; |
| import org.apache.felix.gogo.runtime.Parser.Statement; |
| import org.apache.felix.gogo.runtime.Pipe.Result; |
| import org.apache.felix.service.command.CommandSession; |
| import org.apache.felix.service.command.Function; |
| |
| public class Closure implements Function, Evaluate |
| { |
| |
| public static final String LOCATION = ".location"; |
| public static final String PIPE_EXCEPTION = "pipe-exception"; |
| private static final String DEFAULT_LOCK = ".defaultLock"; |
| |
| private static final ThreadLocal<String> location = new ThreadLocal<>(); |
| |
| private final CommandSessionImpl session; |
| private final Closure parent; |
| private final CharSequence source; |
| private final Program program; |
| private final Object script; |
| |
| private Token errTok; |
| private Token errTok2; |
| private List<Object> parms = null; |
| private List<Object> parmv = null; |
| |
| public Closure(CommandSessionImpl session, Closure parent, CharSequence source) throws Exception |
| { |
| this.session = session; |
| this.parent = parent; |
| this.source = source; |
| this.script = session.get("0"); // by convention, $0 is script name |
| |
| if (source instanceof Program) |
| { |
| program = (Program) source; |
| } |
| else |
| { |
| try |
| { |
| this.program = new Parser(source).program(); |
| } |
| catch (Exception e) |
| { |
| throw setLocation(e); |
| } |
| } |
| } |
| |
| public Closure(CommandSessionImpl session, Closure parent, Program program) |
| { |
| this.session = session; |
| this.parent = parent; |
| this.source = program; |
| this.script = session.get("0"); // by convention, $0 is script name |
| this.program = program; |
| } |
| |
| public CommandSessionImpl session() |
| { |
| return session; |
| } |
| |
| private Exception setLocation(Exception e) |
| { |
| if (session.get(DEFAULT_LOCK) == null) |
| { |
| String loc = location.get(); |
| if (null == loc) |
| { |
| loc = (null == script ? "" : script + ":"); |
| |
| if (e instanceof SyntaxError) |
| { |
| SyntaxError se = (SyntaxError) e; |
| loc += se.line() + "." + se.column(); |
| } |
| else if (null != errTok) |
| { |
| loc += errTok.line + "." + errTok.column; |
| } |
| |
| location.set(loc); |
| } |
| else if (null != script && !loc.contains(":")) |
| { |
| location.set(script + ":" + loc); |
| } |
| |
| session.put(LOCATION, location.get()); |
| } |
| |
| if (e instanceof EOFError) |
| { // map to public exception, so interactive clients can provide more input |
| EOFException eofe = new EOFException(e.getMessage()); |
| eofe.initCause(e); |
| return eofe; |
| } |
| |
| return e; |
| } |
| |
| // implements Function interface |
| public Object execute(CommandSession x, List<Object> values) throws Exception |
| { |
| return execute(x, values, null); |
| } |
| |
| public Object execute(CommandSession x, List<Object> values, Channel capturingOutput) throws Exception |
| { |
| if (x != null && x != session) |
| { |
| if (x instanceof CommandSessionImpl) |
| { |
| return new Closure((CommandSessionImpl) x, null, program).execute(x, values, capturingOutput); |
| } |
| else |
| { |
| throw new IllegalStateException("The current session is not a Gogo session"); |
| } |
| } |
| try |
| { |
| location.remove(); |
| session.put(LOCATION, null); |
| return execute(values, capturingOutput); |
| } |
| catch (Exception e) |
| { |
| throw setLocation(e); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Object execute(List<Object> values, Channel capturingOutput) throws Exception |
| { |
| if (null != values) |
| { |
| parmv = new ArrayList<>(values); |
| parms = new ArgList(parmv); |
| } |
| else if (null != parent) |
| { |
| // inherit parent closure parameters |
| parmv = parent.parmv != null ? new ArrayList<>(parent.parmv) : null; |
| parms = parmv != null ? new ArgList(parmv) : null; |
| } |
| else |
| { |
| // inherit session parameters |
| Object args = session.get("args"); |
| if (null != args && args instanceof List<?>) |
| { |
| parmv = new ArrayList<>((List<Object>) args); |
| parms = new ArgList(parmv); |
| } |
| } |
| |
| Result last = null; |
| Operator operator = null; |
| for (Iterator<Executable> iterator = program.tokens().iterator(); iterator.hasNext();) |
| { |
| Operator prevOperator = operator; |
| Executable executable = iterator.next(); |
| if (iterator.hasNext()) { |
| operator = (Operator) iterator.next(); |
| } else { |
| operator = null; |
| } |
| if (prevOperator != null) { |
| if (Token.eq("&&", prevOperator)) { |
| if (!last.isSuccess()) { |
| continue; |
| } |
| } |
| else if (Token.eq("||", prevOperator)) { |
| if (last.isSuccess()) { |
| continue; |
| } |
| } |
| } |
| |
| Channel[] streams; |
| boolean[] toclose = new boolean[10]; |
| if (Pipe.getCurrentPipe() != null) { |
| streams = Pipe.getCurrentPipe().streams.clone(); |
| } else { |
| streams = new Channel[10]; |
| streams[0] = session.channels[0]; |
| streams[1] = new WritableByteChannelImpl((WritableByteChannel) session.channels[1]); |
| streams[2] = session.channels[1] == session.channels[2] |
| ? streams[1] |
| : new WritableByteChannelImpl((WritableByteChannel) session.channels[2]); |
| } |
| if (capturingOutput != null) { |
| streams[1] = capturingOutput; |
| toclose[1] = true; |
| } |
| |
| CommandSessionImpl.JobImpl job; |
| if (executable instanceof Pipeline) { |
| Pipeline pipeline = (Pipeline) executable; |
| List<Executable> exec = pipeline.tokens(); |
| Token s = exec.get(0); |
| Token e = exec.get(exec.size() - 1); |
| Token t = program.subSequence(s.start - program.start, e.start + e.length - program.start); |
| job = session().createJob(t); |
| for (int i = 0; i < exec.size(); i++) { |
| Statement ex = (Statement) exec.get(i); |
| Operator op = i < exec.size() - 1 ? (Operator) exec.get(++i) : null; |
| Channel[] nstreams; |
| boolean[] ntoclose; |
| boolean endOfPipe; |
| if (i == exec.size() - 1) { |
| nstreams = streams; |
| ntoclose = toclose; |
| endOfPipe = true; |
| } else if (Token.eq("|", op)) { |
| PipedInputStream pis = new PipedInputStream(); |
| PipedOutputStream pos = new PipedOutputStream(pis); |
| nstreams = streams.clone(); |
| nstreams[1] = Channels.newChannel(pos); |
| ntoclose = toclose.clone(); |
| ntoclose[1] = true; |
| streams[0] = Channels.newChannel(pis); |
| toclose[0] = true; |
| endOfPipe = false; |
| } else if (Token.eq("|&", op)) { |
| PipedInputStream pis = new PipedInputStream(); |
| PipedOutputStream pos = new PipedOutputStream(pis); |
| nstreams = streams.clone(); |
| nstreams[1] = nstreams[2] = Channels.newChannel(pos); |
| ntoclose = toclose.clone(); |
| ntoclose[1] = ntoclose[2] = true; |
| streams[0] = Channels.newChannel(pis); |
| toclose[0] = true; |
| endOfPipe = false; |
| } else { |
| throw new IllegalStateException("Unrecognized pipe operator: '" + op + "'"); |
| } |
| Pipe pipe = new Pipe(this, job, ex, nstreams, ntoclose, endOfPipe); |
| job.addPipe(pipe); |
| } |
| } else { |
| job = session().createJob(executable); |
| Pipe pipe = new Pipe(this, job, (Statement) executable, streams, toclose, true); |
| job.addPipe(pipe); |
| } |
| |
| // Start pipe in background |
| if (operator != null && Token.eq("&", operator)) { |
| job.start(Status.Background); |
| last = new Result((Object) null); |
| } |
| // Start in foreground and wait for results |
| else { |
| last = job.start(Status.Foreground); |
| if (last == null) { |
| last = new Result((Object) null); |
| } else if (last.exception != null) { |
| throw last.exception; |
| } |
| } |
| } |
| |
| return last == null ? null : last.result; |
| } |
| |
| private static class WritableByteChannelImpl extends AbstractInterruptibleChannel |
| implements WritableByteChannel { |
| private final WritableByteChannel out; |
| |
| WritableByteChannelImpl(WritableByteChannel out) { |
| this.out = out; |
| } |
| |
| public int write(ByteBuffer src) throws IOException { |
| if (!isOpen()) { |
| throw new ClosedChannelException(); |
| } |
| begin(); |
| try { |
| return out.write(src); |
| } finally { |
| end(true); |
| } |
| } |
| |
| protected void implCloseChannel() { |
| } |
| } |
| |
| static 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 |
| { |
| try |
| { |
| v = s; |
| v = Double.parseDouble(s); // if it parses as double |
| v = Long.parseLong(s); // see whether it is integral |
| } |
| catch (NumberFormatException e) |
| { |
| // Ignore |
| } |
| } |
| return v; |
| } |
| |
| public Object eval(final Token t) throws Exception |
| { |
| return eval(t, true); |
| } |
| |
| public Object eval(final Token t, boolean convertNumeric) throws Exception |
| { |
| if (t instanceof Parser.Closure) |
| { |
| return new Closure(session, this, ((Parser.Closure) t).program()); |
| } |
| else if (t instanceof Sequence) |
| { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| Channel out = Channels.newChannel(baos); |
| Object result = new Closure(session, this, ((Sequence) t).program()) |
| .execute(session, parms, out); |
| if (result != null) { |
| return result; |
| } else { |
| String s = baos.toString(); |
| while (!s.isEmpty() && s.charAt(s.length() - 1) == '\n') { |
| s = s.substring(0, s.length() - 1); |
| } |
| return s; |
| } |
| } |
| else if (t instanceof Array) |
| { |
| return array((Array) t); |
| } |
| else { |
| Object v = Expander.expand(t, this); |
| if (t == v) |
| { |
| if (convertNumeric) |
| { |
| v = eval(v); |
| } |
| } |
| return v; |
| } |
| } |
| |
| public Object execute(Executable executable) throws Exception |
| { |
| if (executable instanceof Statement) |
| { |
| return executeStatement((Statement) executable); |
| } |
| else if (executable instanceof Sequence) |
| { |
| return new Closure(session, this, ((Sequence) executable).program()).execute(new ArrayList<>(), null); |
| } |
| else |
| { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /* |
| * 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(Statement statement) throws Exception |
| { |
| Object echo = session.get("echo"); |
| String xtrace = null; |
| |
| if (echo != null && !"false".equals(echo.toString())) |
| { |
| // set -x execution trace |
| xtrace = "+" + statement; |
| session.perr.println(xtrace); |
| } |
| |
| List<Token> tokens = statement.tokens(); |
| if (tokens.isEmpty()) |
| { |
| return null; |
| } |
| |
| List<Object> values = new ArrayList<>(); |
| errTok = tokens.get(0); |
| |
| if ((tokens.size() > 3) && Token.eq("=", tokens.get(1))) |
| { |
| errTok2 = tokens.get(2); |
| } |
| |
| for (Token t : tokens) |
| { |
| Object v = eval(t, values.isEmpty()); |
| |
| // if ((Token.Type.EXECUTION == t.type) && (tokens.size() == 1)) { |
| // return v; |
| // } |
| |
| if (v instanceof ArgList) |
| { |
| values.addAll((ArgList) v); // explode $args array |
| } |
| else |
| { |
| values.add(v); |
| } |
| } |
| |
| Object cmd = values.remove(0); |
| if (cmd == null) |
| { |
| if (values.isEmpty()) |
| { |
| return null; |
| } |
| |
| throw new RuntimeException("Command name evaluates to null: " + errTok); |
| } |
| |
| if (cmd instanceof CharSequence && values.size() > 0 |
| && Token.eq("=", tokens.get(1))) |
| { |
| values.remove(0); |
| String scmd = cmd.toString(); |
| Object value; |
| |
| if (values.size() == 0) |
| { |
| return session.put(scmd, null); |
| } |
| |
| 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(tokens.get(2), cmd) ? executeCmd(cmd.toString(), values) |
| : executeMethod(cmd, values); |
| } |
| |
| return assignment(scmd, value); |
| } |
| |
| trace2(xtrace, cmd, values); |
| |
| return bareword(tokens.get(0), cmd) ? executeCmd(cmd.toString(), values) |
| : executeMethod(cmd, values); |
| } |
| |
| // second level expanded execution trace |
| private void trace2(String trace1, Object cmd, List<Object> values) |
| { |
| 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.perr.println("+" + trace2); |
| } |
| } |
| } |
| |
| private boolean bareword(Token t, Object v) { |
| return v instanceof CharSequence && Token.eq(t, (CharSequence) v); |
| } |
| |
| 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.put(DEFAULT_LOCK, null); |
| } |
| } |
| } |
| |
| throw new CommandNotFoundException(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))); |
| |
| // 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) |
| { |
| Object target = cmd; |
| ArrayList<Object> args = new ArrayList<>(); |
| values.remove(0); |
| |
| for (Object arg : values) |
| { |
| if (".".equals(arg)) |
| { |
| target = invoke(target, args.remove(0).toString(), args); |
| args.clear(); |
| } |
| else |
| { |
| args.add(arg); |
| } |
| } |
| |
| if (args.size() == 0) |
| { |
| return target; |
| } |
| |
| return invoke(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 invoke(cmd, values.remove(0).toString(), values); |
| } |
| } |
| |
| private Object invoke(Object target, String name, List<Object> args) throws Exception |
| { |
| return session.invoke(target, name, args); |
| } |
| |
| private Object assignment(String name, Object value) |
| { |
| session.put(name, value); |
| return value; |
| } |
| |
| public Object expr(Token expr) |
| { |
| return session.expr(expr); |
| } |
| |
| private Object array(Array array) throws Exception |
| { |
| List<Token> list = array.list(); |
| Map<Token, Token> map = array.map(); |
| |
| if (list != null) |
| { |
| List<Object> olist = new ArrayList<>(); |
| for (Token t : list) |
| { |
| Object oval = eval(t); |
| if (oval.getClass().isArray()) |
| { |
| Collections.addAll(olist, (Object[]) oval); |
| } |
| else if (oval instanceof ArgList) |
| { |
| olist.addAll((ArgList) oval); |
| } |
| else |
| { |
| olist.add(oval); |
| } |
| } |
| return olist; |
| } |
| else |
| { |
| Map<Object, Object> omap = new LinkedHashMap<>(); |
| for (Entry<Token, Token> e : map.entrySet()) |
| { |
| Token key = e.getKey(); |
| Object k = eval(key); |
| if (!(k instanceof String)) |
| { |
| throw new SyntaxError(key.line(), key.column(), |
| "map key null or not String: " + key); |
| } |
| omap.put(k, eval(e.getValue())); |
| } |
| return omap; |
| } |
| } |
| |
| public Object get(String name) |
| { |
| if (parms != null) |
| { |
| if ("args".equals(name)) |
| { |
| return parms; |
| } |
| |
| if ("argv".equals(name)) |
| { |
| return parmv; |
| } |
| |
| if ("it".equals(name)) |
| { |
| return parms.get(0); |
| } |
| |
| if (name.length() == 1 && Character.isDigit(name.charAt(0))) |
| { |
| int i = name.charAt(0) - '0'; |
| if (i > 0) |
| { |
| return parms.get(i - 1); |
| } |
| } |
| } |
| |
| return session.get(name); |
| } |
| |
| public Object put(String key, Object value) |
| { |
| return session.put(key, value); |
| } |
| |
| @Override |
| public Path currentDir() { |
| return isSet(CommandSession.OPTION_NO_GLOB, false) ? null : session().currentDir(); |
| } |
| |
| @Override |
| public ClassLoader classLoader() { |
| return session.classLoader(); |
| } |
| |
| protected boolean isSet(String name, boolean def) { |
| Object v = session.get(name); |
| if (v instanceof Boolean) { |
| return (Boolean) v; |
| } else if (v != null) { |
| String s = v.toString(); |
| return s.isEmpty() || s.equalsIgnoreCase("on") |
| || s.equalsIgnoreCase("1") || s.equalsIgnoreCase("true"); |
| } |
| return def; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return source.toString().trim().replaceAll("\n+", "\n").replaceAll( |
| "([^\\\\{}(\\[])[\\s\n]*\n", "$1;").replaceAll("[ \\\\\t\n]+", " "); |
| } |
| |
| } |