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