| /* |
| * 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.jline; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.PrintStream; |
| import java.io.StringWriter; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.OpenOption; |
| import java.nio.file.Path; |
| import java.nio.file.StandardOpenOption; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.felix.service.command.Job; |
| import org.apache.felix.service.command.Process; |
| import org.apache.felix.gogo.runtime.CommandSessionImpl; |
| import org.apache.felix.service.command.CommandSession; |
| import org.apache.felix.service.command.Converter; |
| import org.apache.felix.service.command.Function; |
| import org.jline.builtins.Commands; |
| import org.jline.builtins.Completers.DirectoriesCompleter; |
| import org.jline.builtins.Completers.FilesCompleter; |
| import org.jline.builtins.Options; |
| import org.jline.reader.Candidate; |
| import org.jline.reader.LineReader; |
| import org.jline.reader.ParsedLine; |
| import org.jline.reader.Widget; |
| import org.jline.terminal.Terminal; |
| |
| import static org.apache.felix.gogo.jline.Shell.getCommands; |
| |
| /** |
| * gosh built-in commands. |
| */ |
| public class Builtin { |
| |
| static final String[] functions = { |
| "format", "getopt", "new", "set", "tac", "type", |
| "jobs", "fg", "bg", |
| "keymap", "setopt", "unsetopt", "complete", "history", "widget", |
| "__files", "__directories", "__usage_completion" |
| }; |
| |
| private static final String[] packages = {"java.lang", "java.io", "java.net", "java.util"}; |
| |
| private final static Set<String> KEYWORDS = new HashSet<>( |
| Arrays.asList("abstract", "continue", "for", "new", "switch", |
| "assert", "default", "goto", "package", "synchronized", "boolean", "do", |
| "if", "private", "this", "break", "double", "implements", "protected", |
| "throw", "byte", "else", "import", "public", "throws", "case", "enum", |
| "instanceof", "return", "transient", "catch", "extends", "int", "short", |
| "try", "char", "final", "interface", "static", "void", "class", |
| "finally", "long", "strictfp", "volatile", "const", "float", "native", |
| "super", "while")); |
| |
| public CharSequence format(CommandSession session) { |
| return format(session, session.get("_")); // last result |
| } |
| |
| public CharSequence format(CommandSession session, Object arg) { |
| Process process = Process.Utils.current(); |
| CharSequence result = session.format(arg, Converter.INSPECT); |
| process.out().println(result); |
| return result; |
| } |
| |
| /** |
| * script access to Options. |
| */ |
| public Options getopt(List<Object> spec, Object[] args) { |
| String[] optSpec = new String[spec.size()]; |
| for (int i = 0; i < optSpec.length; ++i) { |
| optSpec[i] = spec.get(i).toString(); |
| } |
| return Options.compile(optSpec).parse(args); |
| } |
| |
| // FIXME: the "new" command should be provided by runtime, |
| // so it can leverage same argument coercion mechanism, used to invoke methods. |
| public Object _new(CommandSession session, Object name, Object[] argv) throws Exception { |
| Class<?> clazz; |
| |
| if (name instanceof Class<?>) { |
| clazz = (Class<?>) name; |
| } else { |
| clazz = loadClass(session, name.toString()); |
| } |
| |
| for (Constructor<?> c : clazz.getConstructors()) { |
| Class<?>[] types = c.getParameterTypes(); |
| if (types.length != argv.length) { |
| continue; |
| } |
| |
| boolean match = true; |
| |
| Object[] transformed = argv.clone(); |
| for (int i = 0; i < transformed.length; ++i) { |
| try { |
| transformed[i] = session.convert(types[i], transformed[i]); |
| } catch (IllegalArgumentException e) { |
| match = false; |
| break; |
| } |
| } |
| |
| if (!match) { |
| continue; |
| } |
| |
| try { |
| return c.newInstance(transformed); |
| } catch (InvocationTargetException ite) { |
| Throwable cause = ite.getCause(); |
| if (cause instanceof Exception) { |
| throw (Exception) cause; |
| } |
| throw ite; |
| } |
| } |
| |
| throw new IllegalArgumentException("can't coerce " + Arrays.asList(argv) |
| + " to any of " + Arrays.asList(clazz.getConstructors())); |
| } |
| |
| private Class<?> loadClass(CommandSession session, String name) throws ClassNotFoundException { |
| if (!name.contains(".")) { |
| for (String p : packages) { |
| String pkg = p + "." + name; |
| try { |
| return Class.forName(pkg, true, session.classLoader()); |
| } catch (ClassNotFoundException e) { |
| } |
| } |
| } |
| return Class.forName(name, true, session.classLoader()); |
| } |
| |
| public void set(CommandSession session, String[] argv) { |
| final String[] usage = { |
| "set - show session variables", |
| "Usage: set [OPTIONS] [PREFIX]", |
| " -? --help show help", |
| " -a --all show all variables, including those starting with .", |
| " -x set xtrace option", |
| " +x unset xtrace option", |
| "If PREFIX given, then only show variable(s) starting with PREFIX"}; |
| |
| Process process = Process.Utils.current(); |
| Options opt = Options.compile(usage).parse(argv); |
| |
| if (opt.isSet("help")) { |
| opt.usage(process.err()); |
| return; |
| } |
| |
| List<String> args = opt.args(); |
| String prefix = (args.isEmpty() ? "" : args.get(0)); |
| |
| if (opt.isSet("x")) { |
| session.put("echo", true); |
| } else if ("+x".equals(prefix)) { |
| session.put("echo", null); |
| } else { |
| boolean all = opt.isSet("all"); |
| for (String key : new TreeSet<>(Shell.getVariables(session))) { |
| if (!key.startsWith(prefix)) |
| continue; |
| |
| if (key.startsWith(".") && !(all || prefix.length() > 0)) |
| continue; |
| |
| Object target = session.get(key); |
| String type = null; |
| String value = null; |
| |
| if (target != null) { |
| Class<?> clazz = target.getClass(); |
| type = clazz.getSimpleName(); |
| value = target.toString(); |
| } |
| |
| String trunc = value == null || value.length() < 55 ? "" : "..."; |
| process.out().println(String.format("%-15.15s %-15s %.45s%s", type, key, |
| value, trunc)); |
| } |
| } |
| } |
| |
| /* |
| * the following methods depend on the internals of the runtime implementation. |
| * ideally, they should be available via some API. |
| */ |
| |
| public Object tac(CommandSession session, String[] argv) throws IOException { |
| final String[] usage = { |
| "tac - capture stdin as String or List and optionally write to file.", |
| "Usage: tac [-al] [FILE]", |
| " -a --append append to FILE", |
| " -l --list return List<String>", |
| " -? --help show help"}; |
| |
| Process process = Process.Utils.current(); |
| Options opt = Options.compile(usage).parse(argv); |
| |
| if (opt.isSet("help")) { |
| opt.usage(process.err()); |
| return null; |
| } |
| |
| List<String> args = opt.args(); |
| BufferedWriter fw = null; |
| |
| if (args.size() == 1) { |
| Path path = session.currentDir().resolve(args.get(0)); |
| Set<OpenOption> options = new HashSet<>(); |
| options.add(StandardOpenOption.WRITE); |
| options.add(StandardOpenOption.CREATE); |
| if (opt.isSet("append")) { |
| options.add(StandardOpenOption.APPEND); |
| } else { |
| options.add(StandardOpenOption.TRUNCATE_EXISTING); |
| } |
| fw = Files.newBufferedWriter(path, StandardCharsets.UTF_8, options.toArray(new OpenOption[options.size()])); |
| } |
| |
| StringWriter sw = new StringWriter(); |
| BufferedReader rdr = new BufferedReader(new InputStreamReader(process.in())); |
| |
| ArrayList<String> list = null; |
| |
| if (opt.isSet("list")) { |
| list = new ArrayList<>(); |
| } |
| |
| boolean first = true; |
| String s; |
| |
| while ((s = rdr.readLine()) != null) { |
| if (list != null) { |
| list.add(s); |
| } else { |
| if (!first) { |
| sw.write(' '); |
| } |
| first = false; |
| sw.write(s); |
| } |
| |
| if (fw != null) { |
| fw.write(s); |
| fw.newLine(); |
| } |
| } |
| |
| if (fw != null) { |
| fw.close(); |
| } |
| |
| return list != null ? list : sw.toString(); |
| } |
| |
| // FIXME: expose API in runtime so type command doesn't have to duplicate the runtime |
| // command search strategy. |
| public boolean type(CommandSession session, String[] argv) throws Exception { |
| final String[] usage = {"type - show command type", |
| "Usage: type [OPTIONS] [name[:]]", |
| " -a --all show all matches", |
| " -? --help show help", |
| " -q --quiet don't print anything, just return status", |
| " -s --scope=NAME list all commands in named scope", |
| " -t --types show full java type names"}; |
| |
| Process process = Process.Utils.current(); |
| Options opt = Options.compile(usage).parse(argv); |
| List<String> args = opt.args(); |
| |
| if (opt.isSet("help")) { |
| opt.usage(process.err()); |
| return true; |
| } |
| |
| boolean all = opt.isSet("all"); |
| |
| String optScope = null; |
| if (opt.isSet("scope")) { |
| optScope = opt.get("scope"); |
| } |
| |
| if (args.size() == 1) { |
| String arg = args.get(0); |
| if (arg.endsWith(":")) { |
| optScope = args.remove(0); |
| } |
| } |
| |
| if (optScope != null || (args.isEmpty() && all)) { |
| Set<String> snames = new TreeSet<>(); |
| |
| for (String sname : (getCommands(session))) { |
| if ((optScope == null) || sname.startsWith(optScope)) { |
| snames.add(sname); |
| } |
| } |
| |
| for (String sname : snames) { |
| process.out().println(sname); |
| } |
| |
| return true; |
| } |
| |
| if (args.size() == 0) { |
| Map<String, Integer> scopes = new TreeMap<>(); |
| |
| for (String sname : getCommands(session)) { |
| int colon = sname.indexOf(':'); |
| String scope = sname.substring(0, colon); |
| Integer count = scopes.get(scope); |
| if (count == null) { |
| count = 0; |
| } |
| scopes.put(scope, ++count); |
| } |
| |
| for (Entry<String, Integer> entry : scopes.entrySet()) { |
| process.out().println(entry.getKey() + ":" + entry.getValue()); |
| } |
| |
| return true; |
| } |
| |
| final String name = args.get(0).toLowerCase(); |
| |
| final int colon = name.indexOf(':'); |
| final String MAIN = "_main"; // FIXME: must match Reflective.java |
| |
| StringBuilder buf = new StringBuilder(); |
| Set<String> cmds = new LinkedHashSet<>(); |
| |
| // get all commands |
| if ((colon != -1) || (session.get(name) != null)) { |
| cmds.add(name); |
| } else if (session.get(MAIN) != null) { |
| cmds.add(MAIN); |
| } else { |
| String path = session.get("SCOPE") != null ? session.get("SCOPE").toString() |
| : "*"; |
| |
| for (String s : path.split(":")) { |
| if (s.equals("*")) { |
| for (String sname : getCommands(session)) { |
| if (sname.endsWith(":" + name)) { |
| cmds.add(sname); |
| if (!all) { |
| break; |
| } |
| } |
| } |
| } else { |
| String sname = s + ":" + name; |
| if (session.get(sname) != null) { |
| cmds.add(sname); |
| if (!all) { |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| for (String key : cmds) { |
| Object target = session.get(key); |
| if (target == null) { |
| continue; |
| } |
| |
| CharSequence source = getClosureSource(session, key); |
| |
| if (source != null) { |
| buf.append(name); |
| buf.append(" is function {"); |
| buf.append(source); |
| buf.append("}"); |
| continue; |
| } |
| |
| for (Method m : getMethods(session, key)) { |
| StringBuilder params = new StringBuilder(); |
| |
| for (Class<?> type : m.getParameterTypes()) { |
| if (params.length() > 0) { |
| params.append(", "); |
| } |
| params.append(type.getSimpleName()); |
| } |
| |
| String rtype = m.getReturnType().getSimpleName(); |
| |
| if (buf.length() > 0) { |
| buf.append("\n"); |
| } |
| |
| if (opt.isSet("types")) { |
| String cname = m.getDeclaringClass().getName(); |
| buf.append(String.format("%s %s.%s(%s)", rtype, cname, m.getName(), |
| params)); |
| } else { |
| buf.append(String.format("%s is %s %s(%s)", name, rtype, key, params)); |
| } |
| } |
| } |
| |
| if (buf.length() > 0) { |
| if (!opt.isSet("quiet")) { |
| process.out().println(buf); |
| } |
| return true; |
| } |
| |
| if (!opt.isSet("quiet")) { |
| process.err().println("type: " + name + " not found."); |
| } |
| |
| return false; |
| } |
| |
| public void jobs(CommandSession session, String[] argv) { |
| final String[] usage = { |
| "jobs - list jobs", |
| "Usage: jobs [OPTIONS]", |
| " -? --help show help", |
| }; |
| Process process = Process.Utils.current(); |
| Options opt = Options.compile(usage).parse(argv); |
| if (opt.isSet("help")) { |
| opt.usage(process.err()); |
| return; |
| } |
| if (!opt.args().isEmpty()) { |
| process.err().println("usage: jobs"); |
| process.error(2); |
| return; |
| } |
| List<Job> jobs = session.jobs(); |
| Job current = Job.Utils.current(); |
| for (Job job : jobs) { |
| if (job != current) { |
| process.out().println("[" + job.id() + "] " + job.status().toString().toLowerCase() |
| + " " + job.command()); |
| } |
| } |
| } |
| |
| public void fg(CommandSession session, String[] argv) { |
| final String[] usage = { |
| "fg - put job in foreground", |
| "Usage: fg [OPTIONS] [jobid]", |
| " -? --help show help", |
| }; |
| Process process = Process.Utils.current(); |
| Options opt = Options.compile(usage).parse(argv); |
| if (opt.isSet("help")) { |
| opt.usage(process.err()); |
| return; |
| } |
| if (opt.args().size() > 1) { |
| process.err().println("usage: fg [jobid]"); |
| process.error(2); |
| return; |
| } |
| List<Job> jobs = new ArrayList<>(session.jobs()); |
| Collections.reverse(jobs); |
| Job current = Job.Utils.current(); |
| if (argv.length == 0) { |
| Job job = jobs.stream().filter(j -> j != current) |
| .findFirst().orElse(null); |
| if (job != null) { |
| job.foreground(); |
| } else { |
| process.err().println("fg: no current job"); |
| process.error(1); |
| } |
| } else { |
| Job job = jobs.stream().filter(j -> j != current && argv[0].equals(Integer.toString(j.id()))) |
| .findFirst().orElse(null); |
| if (job != null) { |
| job.foreground(); |
| } else { |
| process.err().println("fg: job not found: " + argv[0]); |
| process.error(1); |
| } |
| } |
| } |
| |
| public void bg(CommandSession session, String[] argv) { |
| final String[] usage = { |
| "bg - put job in background", |
| "Usage: bg [OPTIONS] [jobid]", |
| " -? --help show help", |
| }; |
| Process process = Process.Utils.current(); |
| Options opt = Options.compile(usage).parse(argv); |
| if (opt.isSet("help")) { |
| opt.usage(process.err()); |
| return; |
| } |
| if (opt.args().size() > 1) { |
| process.err().println("usage: bg [jobid]"); |
| process.error(2); |
| return; |
| } |
| List<Job> jobs = new ArrayList<>(session.jobs()); |
| Collections.reverse(jobs); |
| Job current = Job.Utils.current(); |
| if (argv.length == 0) { |
| Job job = jobs.stream().filter(j -> j != current) |
| .findFirst().orElse(null); |
| if (job != null) { |
| job.background(); |
| } else { |
| process.err().println("bg: no current job"); |
| process.error(1); |
| } |
| } else { |
| Job job = jobs.stream().filter(j -> j != current && argv[0].equals(Integer.toString(j.id()))) |
| .findFirst().orElse(null); |
| if (job != null) { |
| job.background(); |
| } else { |
| process.err().println("bg: job not found: " + argv[0]); |
| process.error(1); |
| } |
| } |
| } |
| |
| private boolean isClosure(Object target) { |
| return target.getClass().getSimpleName().equals("Closure"); |
| } |
| |
| private boolean isCommand(Object target) { |
| return target.getClass().getSimpleName().equals("CommandProxy"); |
| } |
| |
| private CharSequence getClosureSource(CommandSession session, String name) |
| throws Exception { |
| Object target = session.get(name); |
| |
| if (target == null) { |
| return null; |
| } |
| |
| if (!isClosure(target)) { |
| return null; |
| } |
| |
| Field sourceField = target.getClass().getDeclaredField("source"); |
| sourceField.setAccessible(true); |
| return (CharSequence) sourceField.get(target); |
| } |
| |
| private List<Method> getMethods(CommandSession session, String scmd) throws Exception { |
| final int colon = scmd.indexOf(':'); |
| final String function = colon == -1 ? scmd : scmd.substring(colon + 1); |
| final String name = KEYWORDS.contains(function) ? ("_" + function) : function; |
| final String get = "get" + function; |
| final String is = "is" + function; |
| final String set = "set" + function; |
| final String MAIN = "_main"; // FIXME: must match Reflective.java |
| |
| Object target = session.get(scmd); |
| if (target == null) { |
| return null; |
| } |
| |
| if (isClosure(target)) { |
| return null; |
| } |
| |
| if (isCommand(target)) { |
| Method method = target.getClass().getMethod("getTarget", (Class[]) null); |
| method.setAccessible(true); |
| target = method.invoke(target, (Object[]) null); |
| } |
| |
| ArrayList<Method> list = new ArrayList<>(); |
| Class<?> tc = (target instanceof Class<?>) ? (Class<?>) target |
| : target.getClass(); |
| Method[] methods = tc.getMethods(); |
| |
| for (Method m : methods) { |
| String mname = m.getName().toLowerCase(); |
| |
| if (mname.equals(name) || mname.equals(get) || mname.equals(set) |
| || mname.equals(is) || mname.equals(MAIN)) { |
| list.add(m); |
| } |
| } |
| |
| return list; |
| } |
| |
| public void history(CommandSession session, String[] argv) throws Exception { |
| Process process = Process.Utils.current(); |
| Commands.history(Shell.getReader(session), process.out(), process.err(), null, argv); |
| } |
| |
| public void complete(CommandSession session, String[] argv) throws Exception { |
| Process process = Process.Utils.current(); |
| Commands.complete(Shell.getReader(session), process.out(), process.err(), Shell.getCompletions(session), argv); |
| } |
| |
| public void widget(final CommandSession session, String[] argv) throws Exception { |
| java.util.function.Function<String, Widget> creator = func -> () -> { |
| try { |
| session.execute(func); |
| } catch (Exception e) { |
| // TODO: log exception ? |
| return false; |
| } |
| return true; |
| }; |
| Process process = Process.Utils.current(); |
| Commands.widget(Shell.getReader(session), process.out(), process.err(), creator, argv); |
| } |
| |
| public void keymap(CommandSession session, String[] argv) throws Exception { |
| Process process = Process.Utils.current(); |
| Commands.keymap(Shell.getReader(session), process.out(), process.err(), argv); |
| } |
| |
| public void setopt(CommandSession session, String[] argv) throws Exception { |
| Process process = Process.Utils.current(); |
| Commands.setopt(Shell.getReader(session), process.out(), process.err(), argv); |
| } |
| |
| public void unsetopt(CommandSession session, String[] argv) throws Exception { |
| Process process = Process.Utils.current(); |
| Commands.unsetopt(Shell.getReader(session), process.out(), process.err(), argv); |
| } |
| |
| public List<Candidate> __files(CommandSession session) { |
| ParsedLine line = Shell.getParsedLine(session); |
| LineReader reader = Shell.getReader(session); |
| List<Candidate> candidates = new ArrayList<>(); |
| new FilesCompleter(session.currentDir()) { |
| @Override |
| protected String getDisplay(Terminal terminal, Path p) { |
| return getFileDisplay(session, p); |
| } |
| }.complete(reader, line, candidates); |
| return candidates; |
| } |
| |
| public List<Candidate> __directories(CommandSession session) { |
| ParsedLine line = Shell.getParsedLine(session); |
| LineReader reader = Shell.getReader(session); |
| List<Candidate> candidates = new ArrayList<>(); |
| new DirectoriesCompleter(session.currentDir()) { |
| @Override |
| protected String getDisplay(Terminal terminal, Path p) { |
| return getFileDisplay(session, p); |
| } |
| }.complete(reader, line, candidates); |
| return candidates; |
| } |
| |
| private String getFileDisplay(CommandSession session, Path path) { |
| String type; |
| String suffix; |
| if (Files.isSymbolicLink(path)) { |
| type = "sl"; |
| suffix = "@"; |
| } else if (Files.isDirectory(path)) { |
| type = "dr"; |
| suffix = "/"; |
| } else if (Files.isExecutable(path)) { |
| type = "ex"; |
| suffix = "*"; |
| } else if (!Files.isRegularFile(path)) { |
| type = "ot"; |
| suffix = ""; |
| } else { |
| type = ""; |
| suffix = ""; |
| } |
| return Posix.applyStyle(path.getFileName().toString(), Posix.getLsColorMap(session), type) + suffix; |
| |
| } |
| |
| public void __usage_completion(CommandSession session, String command) throws Exception { |
| Object func = session.get(command.contains(":") ? command : "*:" + command); |
| if (func instanceof Function) { |
| ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| ByteArrayOutputStream baes = new ByteArrayOutputStream(); |
| CommandSession ts = ((CommandSessionImpl) session).processor().createSession(bais, new PrintStream(baos), new PrintStream(baes)); |
| ts.execute(command + " --help"); |
| |
| String regex = "(?x)\\s*" + "(?:-([^-]))?" + // 1: short-opt-1 |
| "(?:,?\\s*-(\\w))?" + // 2: short-opt-2 |
| "(?:,?\\s*--(\\w[\\w-]*)(=\\w+)?)?" + // 3: long-opt-1 and 4:arg-1 |
| "(?:,?\\s*--(\\w[\\w-]*))?" + // 5: long-opt-2 |
| ".*?(?:\\(default=(.*)\\))?\\s*" + // 6: default |
| "(.*)"; // 7: description |
| Pattern pattern = Pattern.compile(regex); |
| for (String l : baes.toString().split("\n")) { |
| Matcher matcher = pattern.matcher(l); |
| if (matcher.matches()) { |
| List<String> args = new ArrayList<>(); |
| if (matcher.group(1) != null) { |
| args.add("--short-option"); |
| args.add(matcher.group(1)); |
| } |
| if (matcher.group(3) != null) { |
| args.add("--long-option"); |
| args.add(matcher.group(1)); |
| } |
| if (matcher.group(4) != null) { |
| args.add("--argument"); |
| args.add(""); |
| } |
| if (matcher.group(7) != null) { |
| args.add("--description"); |
| args.add(matcher.group(7)); |
| } |
| complete(session, args.toArray(new String[args.size()])); |
| } |
| } |
| } |
| } |
| |
| } |