001/*
002 * (C) Copyright 2006-2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General public abstract License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General public abstract License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 */
017package org.nuxeo.shell;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileReader;
022import java.io.FileWriter;
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Properties;
033import java.util.ServiceLoader;
034
035import jline.ANSIBuffer;
036
037import org.nuxeo.shell.cmds.ConfigurationCommands;
038import org.nuxeo.shell.cmds.GlobalCommands;
039import org.nuxeo.shell.cmds.Interactive;
040import org.nuxeo.shell.cmds.Version;
041import org.nuxeo.shell.fs.FileSystem;
042import org.nuxeo.shell.impl.DefaultCompletorProvider;
043import org.nuxeo.shell.impl.DefaultConsole;
044import org.nuxeo.shell.impl.DefaultValueAdapter;
045import org.nuxeo.shell.utils.StringUtils;
046
047/**
048 * There is a single instance of the shell in the VM. To get it call {@link Shell#get()}. parse args if no cmd attempt
049 * to read from stdin a list of cmds or from a faile -f if cmd run it. A cmd line instance is parsing a single command.
050 * parsed data is injected into the command and then the command is run. a cmd type is providing the info on how a
051 * command is injected. top level params are: -h help -u username -p password -f batch file - batch from stdin
052 *
053 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
054 */
055public final class Shell {
056
057    /**
058     * The shell instance
059     */
060    private static volatile Shell shell;
061
062    public static Shell get() {
063        Shell _shell = shell;
064        if (_shell == null) {
065            synchronized (Shell.class) {
066                if (shell == null) {
067                    shell = new Shell();
068                    _shell = shell;
069                }
070            }
071        }
072        return _shell;
073    }
074
075    /**
076     * Reset the shell instance. Useful for embedded shells like applets.
077     */
078    public static synchronized void reset() {
079        shell = null;
080    }
081
082    protected List<ShellConfigurationListener> listeners;
083
084    protected LinkedHashMap<String, String> mainArgs;
085
086    protected CompositeCompletorProvider completorProvider;
087
088    protected CompositeValueAdapter adapter;
089
090    protected ShellConsole console;
091
092    protected Map<String, CommandRegistry> cmds;
093
094    protected CommandRegistry activeRegistry;
095
096    protected Map<String, Object> ctx;
097
098    protected Properties settings;
099
100    protected Map<Class<?>, Object> ctxObjects;
101
102    protected Map<Class<?>, ShellFeature> features;
103
104    /**
105     * A list with all version lines to be displayed when version command is executed.
106     */
107    protected List<String> versions;
108
109    private Shell() {
110        if (shell != null) {
111            throw new ShellException("Shell already loaded");
112        }
113        shell = this;
114        try {
115            loadSettings();
116        } catch (IOException e) {
117            throw new ShellException("Failed to initialize shell", e);
118        }
119        listeners = new ArrayList<ShellConfigurationListener>();
120        features = new HashMap<Class<?>, ShellFeature>();
121        activeRegistry = GlobalCommands.INSTANCE;
122        cmds = new HashMap<String, CommandRegistry>();
123        ctx = new HashMap<String, Object>();
124        ctxObjects = new HashMap<Class<?>, Object>();
125        ctxObjects.put(Shell.class, this);
126        adapter = new CompositeValueAdapter();
127        console = createConsole();
128        completorProvider = new CompositeCompletorProvider();
129        versions = new ArrayList<String>();
130        versions.add("Nuxeo Shell Version: " + Version.getShellVersion());
131        addCompletorProvider(new DefaultCompletorProvider());
132        addValueAdapter(new DefaultValueAdapter());
133        addRegistry(GlobalCommands.INSTANCE);
134        addRegistry(ConfigurationCommands.INSTANCE);
135        loadFeatures();
136    }
137
138    public List<String> getVersions() {
139        return versions;
140    }
141
142    public void addConfigurationListener(ShellConfigurationListener listener) {
143        listeners.add(listener);
144    }
145
146    public void removeConfigurationChangeListener(ShellConfigurationListener listener) {
147        listeners.remove(listener);
148    }
149
150    protected void loadSettings() throws IOException {
151        settings = new Properties();
152        getConfigDir().mkdirs();
153        File file = getSettingsFile();
154        if (file.isFile()) {
155            FileReader reader = new FileReader(getSettingsFile());
156            try {
157                settings.load(reader);
158            } finally {
159                reader.close();
160            }
161        }
162    }
163
164    public Properties getSettings() {
165        return settings;
166    }
167
168    public void setSetting(String name, String value) {
169        try {
170            File file = shell.getSettingsFile();
171            shell.getSettings().put(name, value);
172            FileWriter writer = new FileWriter(file);
173            try {
174                shell.getSettings().store(writer, "generated settings file");
175                for (ShellConfigurationListener listener : listeners) {
176                    listener.onConfigurationChange(name, value);
177                }
178            } finally {
179                writer.close();
180            }
181        } catch (IOException e) {
182            throw new ShellException(e);
183        }
184    }
185
186    public String getSetting(String key) {
187        return settings.getProperty(key);
188    }
189
190    public String getSetting(String key, String defValue) {
191        String v = settings.getProperty(key);
192        return v == null ? defValue : v;
193    }
194
195    public boolean getBooleanSetting(String key, boolean defValue) {
196        String v = settings.getProperty(key);
197        return v == null ? defValue : Boolean.parseBoolean(v);
198    }
199
200    public File getConfigDir() {
201        return new File(System.getProperty("user.home"), ".nxshell");
202    }
203
204    public File getSettingsFile() {
205        return new File(getConfigDir(), "shell.properties");
206    }
207
208    public File getHistoryFile() {
209        return new File(getConfigDir(), "history");
210    }
211
212    protected void loadFeatures() {
213        ServiceLoader<ShellFeature> loader = ServiceLoader.load(ShellFeature.class, Shell.class.getClassLoader());
214        Iterator<ShellFeature> it = loader.iterator();
215        while (it.hasNext()) {
216            addFeature(it.next());
217        }
218        // activate the default feature
219        String ns = System.getProperty("shell");
220        if (ns == null) {
221            ns = getSetting("namespace");
222        }
223        if (ns != null) {
224            CommandRegistry reg = getRegistry(ns);
225            if (reg != null) {
226                setActiveRegistry(ns);
227                return;
228            }
229        }
230        // activate the default built-in namespace
231        setActiveRegistry(getDefaultNamespace());
232    }
233
234    protected String getDefaultNamespace() {
235        if (getRegistry("remote") != null) {
236            return "remote";
237        }
238        if (getRegistry("local") != null) {
239            return "local";
240        }
241        return "global";
242    }
243
244    public LinkedHashMap<String, String> getMainArguments() {
245        return mainArgs;
246    }
247
248    public void main(String[] args) throws Exception {
249        mainArgs = collectArgs(args);
250        String v = mainArgs.get("--version");
251        if (v != null) {
252            System.out.println(Version.getVersionMessage());
253            return;
254        }
255        loadConfig();
256        String path = mainArgs.get("-f");
257        if (path != null) {
258            FileInputStream in = new FileInputStream(new File(path));
259            List<String> lines = null;
260            try {
261                lines = FileSystem.readAndMergeLines(in);
262            } finally {
263                in.close();
264            }
265            runBatch(lines);
266        } else if (mainArgs.get("-e") != null) {
267            String[] cmds = StringUtils.split(mainArgs.get("-e"), ';', true);
268            runBatch(Arrays.asList(cmds));
269        } else if (mainArgs.get("-") != null) { // run batch from stdin
270            List<String> lines = FileSystem.readAndMergeLines(System.in);
271            runBatch(lines);
272        } else {
273            run(Interactive.class.getAnnotation(Command.class).name());
274        }
275    }
276
277    public LinkedHashMap<String, String> collectArgs(String[] args) {
278        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
279        if (args == null || args.length == 0) {
280            return map;
281        }
282        String key = null;
283        int k = 0;
284        for (int i = 0; i < args.length; i++) {
285            if (args[i].startsWith("-")) {
286                if (key != null) {
287                    map.put(key, "true");
288                }
289                key = args[i];
290            } else if (key != null) {
291                map.put(key, args[i]);
292                key = null;
293            } else {
294                map.put("#" + (++k), args[i]);
295                key = null;
296            }
297        }
298        if (key != null) {
299            map.put(key, "true");
300        }
301        return map;
302    }
303
304    public String[] parse(String cmdline) {
305        return parse(cmdline.trim().toCharArray());
306    }
307
308    public String[] parse(char[] cbuf) {
309        ArrayList<String> result = new ArrayList<String>();
310        StringBuilder buf = new StringBuilder();
311        boolean esc = false;
312        char quote = 0;
313        for (int i = 0; i < cbuf.length; i++) {
314            char c = cbuf[i];
315            if (esc) {
316                esc = false;
317                buf.append(c);
318                continue;
319            }
320            switch (c) {
321            case ' ':
322            case '\t':
323            case '\r':
324            case '\n':
325                if (quote != 0) {
326                    buf.append(c);
327                } else if (buf.length() > 0) {
328                    result.add(buf.toString());
329                    buf = new StringBuilder();
330                }
331                break;
332            case '"':
333                if (quote == '"') {
334                    quote = 0;
335                    result.add(buf.toString());
336                    buf = new StringBuilder();
337                } else if (buf.length() > 0) {
338                    buf.append(c);
339                } else {
340                    quote = c;
341                }
342                break;
343            case '\'':
344                if (quote == '\'') {
345                    quote = 0;
346                    result.add(buf.toString());
347                    buf = new StringBuilder();
348                } else if (buf.length() > 0) {
349                    buf.append(c);
350                } else {
351                    quote = c;
352                }
353                break;
354            case '\\':
355                esc = true;
356                break;
357            default:
358                buf.append(c);
359                break;
360            }
361        }
362        if (buf.length() > 0) {
363            result.add(buf.toString());
364        }
365        return result.toArray(new String[result.size()]);
366    }
367
368    protected ShellConsole createConsole() {
369        return new DefaultConsole();
370    }
371
372    public void addValueAdapter(ValueAdapter adapter) {
373        this.adapter.addAdapter(adapter);
374    }
375
376    public void removeValueAdapter(ValueAdapter adapter) {
377        this.adapter.removeAdapter(adapter);
378    }
379
380    public void addCompletorProvider(CompletorProvider provider) {
381        this.completorProvider.addProvider(provider);
382    }
383
384    @SuppressWarnings("unchecked")
385    public <T> T getContextObject(Class<T> type) {
386        return (T) ctxObjects.get(type);
387    }
388
389    public <T> void putContextObject(Class<T> type, T instance) {
390        ctxObjects.put(type, instance);
391    }
392
393    @SuppressWarnings("unchecked")
394    public <T> T removeContextObject(Class<T> type) {
395        return (T) ctxObjects.remove(type);
396    }
397
398    public CompletorProvider getCompletorProvider() {
399        return completorProvider;
400    }
401
402    public void addRegistry(CommandRegistry reg) {
403        cmds.put(reg.getName(), reg);
404    }
405
406    public CommandRegistry removeRegistry(String key) {
407        return cmds.remove(key);
408    }
409
410    public CommandRegistry getRegistry(String name) {
411        return cmds.get(name);
412    }
413
414    public CommandRegistry[] getRegistries() {
415        return cmds.values().toArray(new CommandRegistry[cmds.size()]);
416    }
417
418    public String[] getRegistryNames() {
419        CommandRegistry[] regs = getRegistries();
420        String[] result = new String[regs.length];
421        for (int i = 0; i < regs.length; i++) {
422            result[i] = regs[i].getName();
423        }
424        return result;
425    }
426
427    public CommandRegistry getActiveRegistry() {
428        return activeRegistry;
429    }
430
431    /**
432     * Mark an already registered command registry as the active one.
433     *
434     * @param name
435     * @return
436     */
437    public CommandRegistry setActiveRegistry(String name) {
438        CommandRegistry old = activeRegistry;
439        activeRegistry = getRegistry(name);
440        if (activeRegistry == null) {
441            activeRegistry = old;
442            getConsole().println("No such namespace: " + name);
443            return null;
444        }
445        return old;
446    }
447
448    public ShellConsole getConsole() {
449        return console;
450    }
451
452    public void setConsole(ShellConsole console) {
453        this.console = console;
454    }
455
456    public ValueAdapter getValueAdapter() {
457        return adapter;
458    }
459
460    public Object getProperty(String key) {
461        return ctx.get(key);
462    }
463
464    public Object getProperty(String key, Object defaultValue) {
465        Object v = ctx.get(key);
466        return v == null ? defaultValue : v;
467    }
468
469    public void setProperty(String key, Object value) {
470        ctx.put(key, value);
471    }
472
473    public Map<String, Object> getProperties() {
474        return ctx;
475    }
476
477    public void runBatch(List<String> lines) throws ShellException {
478        for (String line : lines) {
479            run(parse(line));
480        }
481    }
482
483    public void run(String cmdline) throws ShellException {
484        run(parse(cmdline));
485    }
486
487    public void run(String... line) throws ShellException {
488        Runnable cmd = newCommand(line);
489        if (cmd != null) {
490            run(cmd);
491        }
492    }
493
494    public void run(Runnable cmd) throws ShellException {
495        cmd.run();
496    }
497
498    public Runnable newCommand(String cmdline) throws ShellException {
499        return newCommand(parse(cmdline));
500    }
501
502    public Runnable newCommand(String... line) throws ShellException {
503        if (line.length == 0) {
504            return null;
505        }
506        CommandType type = activeRegistry.getCommandType(line[0]);
507        if (type == null) {
508            throw new ShellException("Unknown command: " + line[0]);
509        }
510        return type.newInstance(this, line);
511    }
512
513    public void hello() throws IOException {
514        InputStream in = Shell.class.getClassLoader().getResourceAsStream("META-INF/hello.txt");
515        if (in == null) {
516            getConsole().println("Welcome to " + getClass().getSimpleName() + "!");
517            getConsole().println("Type \"help\" for more information.");
518        } else {
519            try {
520                String content = FileSystem.readContent(in);
521                getConsole().println(content);
522            } finally {
523                in.close();
524            }
525        }
526    }
527
528    public void bye() {
529        console.println("Bye.");
530    }
531
532    public ShellFeature[] getFeatures() {
533        return features.values().toArray(new ShellFeature[features.size()]);
534    }
535
536    @SuppressWarnings("unchecked")
537    public <T extends ShellFeature> T getFeature(Class<T> type) {
538        return (T) features.get(type);
539    }
540
541    public void addFeature(ShellFeature feature) {
542        if (features.containsKey(feature.getClass())) {
543            throw new ShellException("Feature already registered: " + feature.getClass());
544        }
545        feature.install(this);
546        features.put(feature.getClass(), feature);
547    }
548
549    public ANSIBuffer newANSIBuffer() {
550        boolean ansi = false;
551        if (getConsole() instanceof Interactive) {
552            ansi = ((Interactive) getConsole()).getConsole().getTerminal().isANSISupported();
553        }
554        ANSIBuffer buf = new ANSIBuffer();
555        buf.setAnsiEnabled(ansi);
556        return buf;
557    }
558
559    @SuppressWarnings({ "rawtypes", "unchecked" })
560    public void loadConfig() throws IOException {
561        File file = new File(System.getProperty("user.home"), ".nxshell/nxshell.properties");
562        file.getParentFile().mkdirs();
563        if (file.isFile()) {
564            Properties props = new Properties();
565            FileInputStream in = new FileInputStream(file);
566            props.load(in);
567            ctx.putAll((Map) props);
568        }
569    }
570
571}