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}