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}