001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.main; 029 030import org.opencms.configuration.CmsParameterConfiguration; 031import org.opencms.db.CmsUserSettings; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsUser; 034import org.opencms.i18n.CmsLocaleManager; 035import org.opencms.i18n.CmsMessages; 036import org.opencms.security.CmsRole; 037import org.opencms.util.CmsDataTypeUtil; 038import org.opencms.util.CmsFileUtil; 039import org.opencms.util.CmsStringUtil; 040import org.opencms.util.benchmark.CmsBenchmarkTable; 041import org.opencms.util.benchmark.CmsFileBenchmarkReceiver; 042 043import java.awt.event.KeyEvent; 044import java.io.FileDescriptor; 045import java.io.FileInputStream; 046import java.io.IOException; 047import java.io.InputStream; 048import java.io.InputStreamReader; 049import java.io.LineNumberReader; 050import java.io.PrintStream; 051import java.io.Reader; 052import java.io.StreamTokenizer; 053import java.io.StringReader; 054import java.lang.reflect.InvocationTargetException; 055import java.lang.reflect.Method; 056import java.lang.reflect.Modifier; 057import java.util.ArrayList; 058import java.util.Collection; 059import java.util.Iterator; 060import java.util.List; 061import java.util.Locale; 062import java.util.Map; 063import java.util.TreeMap; 064 065/** 066 * A command line interface to access OpenCms functions which 067 * is used for the initial setup and also can be used for scripting access to the OpenCms 068 * repository without the Workplace.<p> 069 * 070 * The CmsShell has direct access to all methods in the "command objects". 071 * Currently the following classes are used as command objects: 072 * <code>{@link org.opencms.main.CmsShellCommands}</code>, 073 * <code>{@link org.opencms.file.CmsRequestContext}</code> and 074 * <code>{@link org.opencms.file.CmsObject}</code>.<p> 075 * 076 * It is also possible to add a custom command object when calling the script API, 077 * like in {@link CmsShell#CmsShell(String, String, String, String, I_CmsShellCommands, PrintStream, PrintStream, boolean)}.<p> 078 * 079 * Only public methods in the command objects that use supported data types 080 * as parameters can be called from the shell. Supported data types are: 081 * <code>String, {@link org.opencms.util.CmsUUID}, boolean, int, long, double, float</code>.<p> 082 * 083 * If a method name is ambiguous, i.e. the method name with the same number of parameter exist 084 * in more then one of the command objects, the method is only executed on the first matching method object.<p> 085 * 086 * @since 6.0.0 087 * 088 * @see org.opencms.main.CmsShellCommands 089 * @see org.opencms.file.CmsRequestContext 090 * @see org.opencms.file.CmsObject 091 */ 092public class CmsShell { 093 094 /** 095 * Command object class.<p> 096 */ 097 private class CmsCommandObject { 098 099 /** The list of methods. */ 100 private Map<String, List<Method>> m_methods; 101 102 /** The object to execute the methods on. */ 103 private Object m_object; 104 105 /** 106 * Creates a new command object.<p> 107 * 108 * @param object the object to execute the methods on 109 */ 110 protected CmsCommandObject(Object object) { 111 112 m_object = object; 113 initShellMethods(); 114 } 115 116 /** 117 * Tries to execute a method for the provided parameters on this command object.<p> 118 * 119 * If methods with the same name and number of parameters exist in this command object, 120 * the given parameters are tried to be converted from String to matching types.<p> 121 * 122 * @param command the command entered by the user in the shell 123 * @param parameters the parameters entered by the user in the shell 124 * @return true if a method was executed, false otherwise 125 */ 126 @SuppressWarnings("synthetic-access") 127 protected boolean executeMethod(String command, List<String> parameters) { 128 129 m_hasReportError = false; 130 // build the method lookup 131 String lookup = buildMethodLookup(command, parameters.size()); 132 133 // try to look up the methods of this command object 134 List<Method> possibleMethods = m_methods.get(lookup); 135 if (possibleMethods == null) { 136 return false; 137 } 138 139 // a match for the method name was found, now try to figure out if the parameters are ok 140 Method onlyStringMethod = null; 141 Method foundMethod = null; 142 Object[] params = null; 143 Iterator<Method> i; 144 145 // first check if there is one method with only has String parameters, make this the fall back 146 i = possibleMethods.iterator(); 147 while (i.hasNext()) { 148 Method method = i.next(); 149 Class<?>[] clazz = method.getParameterTypes(); 150 boolean onlyString = true; 151 for (int j = 0; j < clazz.length; j++) { 152 if (!(clazz[j].equals(String.class))) { 153 onlyString = false; 154 break; 155 } 156 } 157 if (onlyString) { 158 onlyStringMethod = method; 159 break; 160 } 161 } 162 163 // now check a method matches the provided parameters 164 // if so, use this method, else continue searching 165 i = possibleMethods.iterator(); 166 while (i.hasNext()) { 167 Method method = i.next(); 168 if (method == onlyStringMethod) { 169 // skip the String only signature because this would always match 170 continue; 171 } 172 // now try to convert the parameters to the required types 173 Class<?>[] clazz = method.getParameterTypes(); 174 Object[] converted = new Object[clazz.length]; 175 boolean match = true; 176 for (int j = 0; j < clazz.length; j++) { 177 String value = parameters.get(j); 178 try { 179 converted[j] = CmsDataTypeUtil.parse(value, clazz[j]); 180 } catch (Throwable t) { 181 match = false; 182 break; 183 } 184 } 185 if (match) { 186 // we found a matching method signature 187 params = converted; 188 foundMethod = method; 189 break; 190 } 191 192 } 193 194 if ((foundMethod == null) && (onlyStringMethod != null)) { 195 // no match found but String only signature available, use this 196 params = parameters.toArray(); 197 foundMethod = onlyStringMethod; 198 } 199 200 if ((params == null) || (foundMethod == null)) { 201 // no match found at all 202 return false; 203 } 204 205 // now try to invoke the method 206 try { 207 Object result = foundMethod.invoke(m_object, params); 208 if (result != null) { 209 if (result instanceof Collection<?>) { 210 Collection<?> c = (Collection<?>)result; 211 m_out.println(c.getClass().getName() + " (size: " + c.size() + ")"); 212 int count = 0; 213 if (result instanceof Map<?, ?>) { 214 Map<?, ?> m = (Map<?, ?>)result; 215 Iterator<?> j = m.entrySet().iterator(); 216 while (j.hasNext()) { 217 Map.Entry<?, ?> entry = (Map.Entry<?, ?>)j.next(); 218 m_out.println(count++ + ": " + entry.getKey() + "= " + entry.getValue()); 219 } 220 } else { 221 Iterator<?> j = c.iterator(); 222 while (j.hasNext()) { 223 m_out.println(count++ + ": " + j.next()); 224 } 225 } 226 } else { 227 m_out.println(result.toString()); 228 } 229 } 230 } catch (InvocationTargetException ite) { 231 m_out.println( 232 Messages.get().getBundle(getLocale()).key( 233 Messages.GUI_SHELL_EXEC_METHOD_1, 234 new Object[] {foundMethod.getName()})); 235 ite.getTargetException().printStackTrace(m_out); 236 if (m_errorCode != -1) { 237 throw new CmsShellCommandException(ite.getCause()); 238 } 239 } catch (Throwable t) { 240 m_out.println( 241 Messages.get().getBundle(getLocale()).key( 242 Messages.GUI_SHELL_EXEC_METHOD_1, 243 new Object[] {foundMethod.getName()})); 244 t.printStackTrace(m_out); 245 if (m_errorCode != -1) { 246 throw new CmsShellCommandException(t); 247 } 248 } 249 if (m_hasReportError && (m_errorCode != -1)) { 250 throw new CmsShellCommandException(true); 251 } 252 return true; 253 } 254 255 /** 256 * Returns a signature overview of all methods containing the given search String.<p> 257 * 258 * If no method name matches the given search String, the empty String is returned.<p> 259 * 260 * @param searchString the String to search for, if null all methods are shown 261 * 262 * @return a signature overview of all methods containing the given search String 263 */ 264 protected String getMethodHelp(String searchString) { 265 266 StringBuffer buf = new StringBuffer(512); 267 Iterator<String> i = m_methods.keySet().iterator(); 268 while (i.hasNext()) { 269 List<Method> l = m_methods.get(i.next()); 270 Iterator<Method> j = l.iterator(); 271 while (j.hasNext()) { 272 Method method = j.next(); 273 if ((searchString == null) 274 || (method.getName().toLowerCase().indexOf(searchString.toLowerCase()) > -1)) { 275 buf.append("* "); 276 buf.append(method.getName()); 277 buf.append("("); 278 Class<?>[] params = method.getParameterTypes(); 279 for (int k = 0; k < params.length; k++) { 280 String par = params[k].getName(); 281 par = par.substring(par.lastIndexOf('.') + 1); 282 if (k != 0) { 283 buf.append(", "); 284 } 285 buf.append(par); 286 } 287 buf.append(")\n"); 288 } 289 } 290 } 291 return buf.toString(); 292 } 293 294 /** 295 * Returns the object to execute the methods on.<p> 296 * 297 * @return the object to execute the methods on 298 */ 299 protected Object getObject() { 300 301 return m_object; 302 } 303 304 /** 305 * Builds a method lookup String.<p> 306 * 307 * @param methodName the name of the method 308 * @param paramCount the parameter count of the method 309 * 310 * @return a method lookup String 311 */ 312 private String buildMethodLookup(String methodName, int paramCount) { 313 314 StringBuffer buf = new StringBuffer(32); 315 buf.append(methodName.toLowerCase()); 316 buf.append(" ["); 317 buf.append(paramCount); 318 buf.append("]"); 319 return buf.toString(); 320 } 321 322 /** 323 * Initializes the map of accessible methods.<p> 324 */ 325 private void initShellMethods() { 326 327 Map<String, List<Method>> result = new TreeMap<String, List<Method>>(); 328 329 Method[] methods = m_object.getClass().getMethods(); 330 for (int i = 0; i < methods.length; i++) { 331 // only public methods directly declared in the base class can be used in the shell 332 if ((methods[i].getDeclaringClass() == m_object.getClass()) 333 && (methods[i].getModifiers() == Modifier.PUBLIC)) { 334 335 // check if the method signature only uses primitive data types 336 boolean onlyPrimitive = true; 337 Class<?>[] clazz = methods[i].getParameterTypes(); 338 for (int j = 0; j < clazz.length; j++) { 339 if (!CmsDataTypeUtil.isParseable(clazz[j])) { 340 // complex data type methods can not be called from the shell 341 onlyPrimitive = false; 342 break; 343 } 344 } 345 346 if (onlyPrimitive) { 347 // add this method to the set of methods that can be called from the shell 348 String lookup = buildMethodLookup(methods[i].getName(), methods[i].getParameterTypes().length); 349 List<Method> l; 350 if (result.containsKey(lookup)) { 351 l = result.get(lookup); 352 } else { 353 l = new ArrayList<Method>(1); 354 } 355 l.add(methods[i]); 356 result.put(lookup, l); 357 } 358 } 359 } 360 m_methods = result; 361 } 362 } 363 364 /** Prefix for "additional" parameter. */ 365 public static final String SHELL_PARAM_ADDITIONAL_COMMANDS = "-additional="; 366 367 /** Prefix for "base" parameter. */ 368 public static final String SHELL_PARAM_BASE = "-base="; 369 370 /** Prefix for "servletMapping" parameter. */ 371 public static final String SHELL_PARAM_DEFAULT_WEB_APP = "-defaultWebApp="; 372 373 /** Prefix for errorCode parameter. */ 374 public static final String SHELL_PARAM_ERROR_CODE = "-errorCode="; 375 376 /** Command line parameter to prevent disabling of JLAN. */ 377 public static final String SHELL_PARAM_JLAN = "-jlan"; 378 379 /** Prefix for "script" parameter. */ 380 public static final String SHELL_PARAM_SCRIPT = "-script="; 381 382 /** Prefix for "servletMapping" parameter. */ 383 public static final String SHELL_PARAM_SERVLET_MAPPING = "-servletMapping="; 384 385 /** 386 * Thread local which stores the currently active shell instance. 387 * 388 * <p>We need multiple ones because shell commands may cause another nested shell to be launched (e.g. for module import scripts). 389 */ 390 public static final ThreadLocal<ArrayList<CmsShell>> SHELL_STACK = ThreadLocal.withInitial(() -> new ArrayList<>()); 391 392 /** Boolean variable to disable JLAN. */ 393 private static boolean JLAN_DISABLED; 394 395 /** The benchmark table. */ 396 private CmsBenchmarkTable m_benchmarkTable; 397 398 /** The OpenCms context object. */ 399 protected CmsObject m_cms; 400 401 /** Stream to write the error messages output to. */ 402 protected PrintStream m_err; 403 404 /** The code which the process should exit with in case of errors; -1 means exit is not called. */ 405 protected int m_errorCode = -1; 406 407 /** Stream to write the regular output messages to. */ 408 protected PrintStream m_out; 409 410 /** Additional shell commands object. */ 411 private I_CmsShellCommands m_additionalShellCommands; 412 413 /** All shell callable objects. */ 414 private List<CmsCommandObject> m_commandObjects; 415 416 /** If set to true, all commands are echoed. */ 417 private boolean m_echo; 418 419 /** Indicates if the 'exit' command has been called. */ 420 private boolean m_exitCalled; 421 422 /** Flag to indicate whether an error was added to a shell report during the last command execution. */ 423 private boolean m_hasReportError; 424 425 /** Indicates if this is an interactive session with a user sitting on a console. */ 426 private boolean m_interactive; 427 428 /** The messages object. */ 429 private CmsMessages m_messages; 430 431 /** The OpenCms system object. */ 432 private OpenCmsCore m_opencms; 433 434 /** The shell prompt format. */ 435 private String m_prompt; 436 437 /** The current users settings. */ 438 private CmsUserSettings m_settings; 439 440 /** Internal shell command object. */ 441 private I_CmsShellCommands m_shellCommands; 442 443 /** 444 * Creates a new CmsShell.<p> 445 * 446 * @param cms the user context to run the shell from 447 * @param prompt the prompt format to set 448 * @param additionalShellCommands optional object for additional shell commands, or null 449 * @param out stream to write the regular output messages to 450 * @param err stream to write the error messages output to 451 */ 452 public CmsShell( 453 CmsObject cms, 454 String prompt, 455 I_CmsShellCommands additionalShellCommands, 456 PrintStream out, 457 PrintStream err) { 458 459 setPrompt(prompt); 460 try { 461 // has to be initialized already if this constructor is used 462 m_opencms = null; 463 Locale locale = getLocale(); 464 m_messages = Messages.get().getBundle(locale); 465 m_cms = cms; 466 467 // initialize the shell 468 initShell(additionalShellCommands, out, err); 469 } catch (Throwable t) { 470 t.printStackTrace(m_err); 471 } 472 } 473 474 /** 475 * Creates a new CmsShell using System.out and System.err for output of the messages.<p> 476 * 477 * @param webInfPath the path to the 'WEB-INF' folder of the OpenCms installation 478 * @param servletMapping the mapping of the servlet (or <code>null</code> to use the default <code>"/opencms/*"</code>) 479 * @param defaultWebAppName the name of the default web application (or <code>null</code> to use the default <code>"ROOT"</code>) 480 * @param prompt the prompt format to set 481 * @param additionalShellCommands optional object for additional shell commands, or null 482 */ 483 public CmsShell( 484 String webInfPath, 485 String servletMapping, 486 String defaultWebAppName, 487 String prompt, 488 I_CmsShellCommands additionalShellCommands) { 489 490 this( 491 webInfPath, 492 servletMapping, 493 defaultWebAppName, 494 prompt, 495 additionalShellCommands, 496 System.out, 497 System.err, 498 false); 499 } 500 501 /** 502 * Creates a new CmsShell.<p> 503 * 504 * @param webInfPath the path to the 'WEB-INF' folder of the OpenCms installation 505 * @param servletMapping the mapping of the servlet (or <code>null</code> to use the default <code>"/opencms/*"</code>) 506 * @param defaultWebAppName the name of the default web application (or <code>null</code> to use the default <code>"ROOT"</code>) 507 * @param prompt the prompt format to set 508 * @param additionalShellCommands optional object for additional shell commands, or null 509 * @param out stream to write the regular output messages to 510 * @param err stream to write the error messages output to 511 * @param interactive if <code>true</code> this is an interactive session with a user sitting on a console 512 */ 513 public CmsShell( 514 String webInfPath, 515 String servletMapping, 516 String defaultWebAppName, 517 String prompt, 518 I_CmsShellCommands additionalShellCommands, 519 PrintStream out, 520 PrintStream err, 521 boolean interactive) { 522 523 setPrompt(prompt); 524 setInteractive(interactive); 525 if (CmsStringUtil.isEmpty(servletMapping)) { 526 servletMapping = "/opencms/*"; 527 } 528 if (CmsStringUtil.isEmpty(defaultWebAppName)) { 529 defaultWebAppName = "ROOT"; 530 } 531 try { 532 // first initialize runlevel 1 533 m_opencms = OpenCmsCore.getInstance(); 534 // Externalization: get Locale: will be the System default since no CmsObject is up before 535 // runlevel 2 536 Locale locale = getLocale(); 537 m_messages = Messages.get().getBundle(locale); 538 // search for the WEB-INF folder 539 if (CmsStringUtil.isEmpty(webInfPath)) { 540 out.println(m_messages.key(Messages.GUI_SHELL_NO_HOME_FOLDER_SPECIFIED_0)); 541 out.println(); 542 webInfPath = CmsFileUtil.searchWebInfFolder(System.getProperty("user.dir")); 543 if (CmsStringUtil.isEmpty(webInfPath)) { 544 err.println(m_messages.key(Messages.GUI_SHELL_HR_0)); 545 err.println(m_messages.key(Messages.GUI_SHELL_NO_HOME_FOLDER_FOUND_0)); 546 err.println(); 547 err.println(m_messages.key(Messages.GUI_SHELL_START_DIR_LINE1_0)); 548 err.println(m_messages.key(Messages.GUI_SHELL_START_DIR_LINE2_0)); 549 err.println(m_messages.key(Messages.GUI_SHELL_HR_0)); 550 return; 551 } 552 } 553 out.println(Messages.get().getBundle(locale).key(Messages.GUI_SHELL_WEB_INF_PATH_1, webInfPath)); 554 // set the path to the WEB-INF folder (the 2nd and 3rd parameters are just reasonable dummies) 555 CmsServletContainerSettings settings = new CmsServletContainerSettings( 556 webInfPath, 557 defaultWebAppName, 558 servletMapping, 559 null, 560 null); 561 m_opencms.getSystemInfo().init(settings); 562 // now read the configuration properties 563 String propertyPath = m_opencms.getSystemInfo().getConfigurationFileRfsPath(); 564 out.println(m_messages.key(Messages.GUI_SHELL_CONFIG_FILE_1, propertyPath)); 565 out.println(); 566 CmsParameterConfiguration configuration = new CmsParameterConfiguration(propertyPath); 567 568 // now upgrade to runlevel 2 569 m_opencms = m_opencms.upgradeRunlevel(configuration); 570 571 // create a context object with 'Guest' permissions 572 m_cms = m_opencms.initCmsObject(m_opencms.getDefaultUsers().getUserGuest()); 573 574 // initialize the shell 575 initShell(additionalShellCommands, out, err); 576 } catch (Throwable t) { 577 t.printStackTrace(err); 578 } 579 } 580 581 /** 582 * Gets the top of thread-local shell stack, or null if it is empty. 583 * 584 * @return the top of the shell stack 585 */ 586 public static CmsShell getTopShell() { 587 588 ArrayList<CmsShell> shells = SHELL_STACK.get(); 589 if (shells.isEmpty()) { 590 return null; 591 } 592 return shells.get(shells.size() - 1); 593 594 } 595 596 /** 597 * Check if JLAN should be disabled.<p> 598 * 599 * @return true if JLAN should be disabled 600 */ 601 public static boolean isJlanDisabled() { 602 603 return JLAN_DISABLED; 604 } 605 606 /** 607 * Main program entry point when started via the command line.<p> 608 * 609 * @param args parameters passed to the application via the command line 610 */ 611 public static void main(String[] args) { 612 613 JLAN_DISABLED = true; 614 boolean wrongUsage = false; 615 String webInfPath = null; 616 String script = null; 617 String servletMapping = null; 618 String defaultWebApp = null; 619 String additional = null; 620 int errorCode = -1; 621 if (args.length > 4) { 622 wrongUsage = true; 623 } else { 624 for (int i = 0; i < args.length; i++) { 625 String arg = args[i]; 626 if (arg.startsWith(SHELL_PARAM_BASE)) { 627 webInfPath = arg.substring(SHELL_PARAM_BASE.length()); 628 } else if (arg.startsWith(SHELL_PARAM_SCRIPT)) { 629 script = arg.substring(SHELL_PARAM_SCRIPT.length()); 630 } else if (arg.startsWith(SHELL_PARAM_SERVLET_MAPPING)) { 631 servletMapping = arg.substring(SHELL_PARAM_SERVLET_MAPPING.length()); 632 } else if (arg.startsWith(SHELL_PARAM_DEFAULT_WEB_APP)) { 633 defaultWebApp = arg.substring(SHELL_PARAM_DEFAULT_WEB_APP.length()); 634 } else if (arg.startsWith(SHELL_PARAM_ADDITIONAL_COMMANDS)) { 635 additional = arg.substring(SHELL_PARAM_ADDITIONAL_COMMANDS.length()); 636 } else if (arg.startsWith(SHELL_PARAM_ERROR_CODE)) { 637 errorCode = Integer.valueOf(arg.substring(SHELL_PARAM_ERROR_CODE.length())).intValue(); 638 } else if (arg.startsWith(SHELL_PARAM_JLAN)) { 639 JLAN_DISABLED = false; 640 } else { 641 System.out.println(Messages.get().getBundle().key(Messages.GUI_SHELL_WRONG_USAGE_0)); 642 wrongUsage = true; 643 } 644 } 645 } 646 if (wrongUsage) { 647 System.out.println(Messages.get().getBundle().key(Messages.GUI_SHELL_USAGE_1, CmsShell.class.getName())); 648 } else { 649 650 I_CmsShellCommands additionalCommands = null; 651 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(additional)) { 652 try { 653 Class<?> commandClass = Class.forName(additional); 654 additionalCommands = (I_CmsShellCommands)commandClass.newInstance(); 655 } catch (Exception e) { 656 System.out.println( 657 Messages.get().getBundle().key(Messages.GUI_SHELL_ERR_ADDITIONAL_COMMANDS_1, additional)); 658 e.printStackTrace(); 659 return; 660 } 661 } 662 boolean interactive = true; 663 FileInputStream stream = null; 664 if (script != null) { 665 try { 666 stream = new FileInputStream(script); 667 } catch (IOException exc) { 668 System.out.println(Messages.get().getBundle().key(Messages.GUI_SHELL_ERR_SCRIPTFILE_1, script)); 669 } 670 } 671 if (stream == null) { 672 // no script-file, use standard input stream 673 stream = new FileInputStream(FileDescriptor.in); 674 interactive = true; 675 } 676 CmsShell shell = new CmsShell( 677 webInfPath, 678 servletMapping, 679 defaultWebApp, 680 "${user}@${project}:${siteroot}|${uri}>", 681 additionalCommands, 682 System.out, 683 System.err, 684 interactive); 685 shell.m_errorCode = errorCode; 686 shell.execute(stream); 687 try { 688 stream.close(); 689 } catch (IOException e) { 690 e.printStackTrace(); 691 } 692 } 693 } 694 695 /** 696 * Removes top of thread-local shell stack. 697 */ 698 public static void popShell() { 699 700 ArrayList<CmsShell> shells = SHELL_STACK.get(); 701 if (shells.size() > 0) { 702 shells.remove(shells.size() - 1); 703 } 704 705 } 706 707 /** 708 * Pushes shell instance on thread-local stack. 709 * 710 * @param shell the shell to push 711 */ 712 public static void pushShell(CmsShell shell) { 713 714 SHELL_STACK.get().add(shell); 715 } 716 717 /** 718 * If running in the context of a CmsShell, this method notifies the running shell instance that an error has occured in a report.<p> 719 */ 720 public static void setReportError() { 721 722 CmsShell instance = getTopShell(); 723 if (instance != null) { 724 instance.m_hasReportError = true; 725 } 726 } 727 728 /** 729 * Executes the commands from the given input stream in this shell.<p> 730 * 731 * <ul> 732 * <li>Commands in the must be separated with a line break '\n'. 733 * <li>Only one command per line is allowed. 734 * <li>String parameters must be quoted like this: <code>'string value'</code>. 735 * </ul> 736 * 737 * @param inputStream the input stream from which the commands are read 738 */ 739 public void execute(InputStream inputStream) { 740 741 execute(new InputStreamReader(inputStream)); 742 } 743 744 /** 745 * Executes the commands from the given reader in this shell.<p> 746 * 747 * <ul> 748 * <li>Commands in the must be separated with a line break '\n'. 749 * <li>Only one command per line is allowed. 750 * <li>String parameters must be quoted like this: <code>'string value'</code>. 751 * </ul> 752 * 753 * @param reader the reader from which the commands are read 754 */ 755 public void execute(Reader reader) { 756 757 try { 758 pushShell(this); 759 LineNumberReader lnr = new LineNumberReader(reader); 760 while (!m_exitCalled) { 761 String line = lnr.readLine(); 762 if (m_interactive || m_echo) { 763 // print the prompt in front of the commands to process only when 'interactive' 764 if ((line != null) | m_interactive) { 765 printPrompt(); 766 } 767 } 768 769 if (line == null) { 770 // if null the file has been read to the end 771 if (m_interactive) { 772 try { 773 Thread.sleep(500); 774 } catch (Throwable t) { 775 // noop 776 } 777 } 778 // end the while loop 779 break; 780 } 781 if (line.trim().startsWith("#")) { 782 m_out.println(line); 783 continue; 784 } 785 // In linux, the up and down arrows generate escape sequences that cannot be properly handled. 786 // If a escape sequence is detected, OpenCms prints a warning message 787 if (line.indexOf(KeyEvent.VK_ESCAPE) != -1) { 788 m_out.println(m_messages.key(Messages.GUI_SHELL_ESCAPE_SEQUENCES_NOT_SUPPORTED_0)); 789 continue; 790 } 791 StringReader lineReader = new StringReader(line); 792 StreamTokenizer st = new StreamTokenizer(lineReader); 793 st.eolIsSignificant(true); 794 st.wordChars('*', '*'); 795 // put all tokens into a List 796 List<String> parameters = new ArrayList<String>(); 797 while (st.nextToken() != StreamTokenizer.TT_EOF) { 798 if (st.ttype == StreamTokenizer.TT_NUMBER) { 799 parameters.add(Integer.toString(new Double(st.nval).intValue())); 800 } else { 801 if (null != st.sval) { 802 parameters.add(st.sval); 803 } 804 } 805 } 806 lineReader.close(); 807 808 if (parameters.size() == 0) { 809 // empty line, just need to check if echo is on 810 if (m_echo) { 811 m_out.println(); 812 } 813 continue; 814 } 815 816 // extract command and arguments 817 String command = parameters.get(0); 818 List<String> arguments = parameters.subList(1, parameters.size()); 819 820 // execute the command with the given arguments 821 executeCommand(command, arguments); 822 } 823 } catch (Throwable t) { 824 if (!(t instanceof CmsShellCommandException)) { 825 // in case it's a shell command exception, the stack trace has already been written 826 t.printStackTrace(m_err); 827 } 828 if (m_errorCode != -1) { 829 System.exit(m_errorCode); 830 } 831 } finally { 832 popShell(); 833 } 834 } 835 836 /** 837 * Executes the commands from the given string in this shell.<p> 838 * 839 * <ul> 840 * <li>Commands in the must be separated with a line break '\n'. 841 * <li>Only one command per line is allowed. 842 * <li>String parameters must be quoted like this: <code>'string value'</code>. 843 * </ul> 844 * 845 * @param commands the string from which the commands are read 846 */ 847 public void execute(String commands) { 848 849 execute(new StringReader(commands)); 850 } 851 852 /** 853 * Executes a shell command with a list of parameters.<p> 854 * 855 * @param command the command to execute 856 * @param parameters the list of parameters for the command 857 */ 858 public void executeCommand(String command, List<String> parameters) { 859 860 if (null == command) { 861 return; 862 } 863 864 if (m_echo) { 865 // echo the command to STDOUT 866 m_out.print(command); 867 for (int i = 0; i < parameters.size(); i++) { 868 m_out.print(" '"); 869 m_out.print(parameters.get(i)); 870 m_out.print("'"); 871 } 872 m_out.println(); 873 } 874 875 // prepare to lookup a method in CmsObject or CmsShellCommands 876 boolean executed = false; 877 Iterator<CmsCommandObject> i = m_commandObjects.iterator(); 878 while (!executed && i.hasNext()) { 879 CmsCommandObject cmdObj = i.next(); 880 executed = cmdObj.executeMethod(command, parameters); 881 } 882 883 if (!executed) { 884 // method not found 885 m_out.println(); 886 StringBuffer commandMsg = new StringBuffer(command).append("("); 887 for (int j = 0; j < parameters.size(); j++) { 888 commandMsg.append("value"); 889 if (j < (parameters.size() - 1)) { 890 commandMsg.append(", "); 891 } 892 } 893 commandMsg.append(")"); 894 895 m_out.println(m_messages.key(Messages.GUI_SHELL_METHOD_NOT_FOUND_1, commandMsg.toString())); 896 m_out.println(m_messages.key(Messages.GUI_SHELL_HR_0)); 897 ((CmsShellCommands)m_shellCommands).help(); 898 } 899 } 900 901 /** 902 * Exits this shell and destroys the OpenCms instance.<p> 903 */ 904 public void exit() { 905 906 if (m_exitCalled) { 907 return; 908 } 909 m_exitCalled = true; 910 try { 911 if (m_additionalShellCommands != null) { 912 m_additionalShellCommands.shellExit(); 913 } else if (null != m_shellCommands) { 914 m_shellCommands.shellExit(); 915 } 916 } catch (Throwable t) { 917 t.printStackTrace(); 918 } 919 if (m_opencms != null) { 920 // if called by an in line script we don't want to kill the whole instance 921 try { 922 m_opencms.shutDown(); 923 } catch (Throwable t) { 924 t.printStackTrace(); 925 } 926 } 927 } 928 929 /** 930 * Gets the benchmark table for the shell, lazily initializing it if it didn't exist yet. 931 * 932 * @return the benchmark table for the shell. 933 */ 934 public CmsBenchmarkTable getBenchmarkTable() { 935 936 if (m_benchmarkTable == null) { 937 m_benchmarkTable = new CmsBenchmarkTable(new CmsFileBenchmarkReceiver()); 938 } 939 return m_benchmarkTable; 940 } 941 942 /** 943 * Returns the stream this shell writes its error messages to.<p> 944 * 945 * @return the stream this shell writes its error messages to 946 */ 947 public PrintStream getErr() { 948 949 return m_err; 950 } 951 952 /** 953 * Gets the error code.<p> 954 * 955 * @return the error code 956 */ 957 public int getErrorCode() { 958 959 return m_errorCode; 960 } 961 962 /** 963 * Private internal helper for localization to the current user's locale 964 * within OpenCms. <p> 965 * 966 * @return the current user's <code>Locale</code>. 967 */ 968 public Locale getLocale() { 969 970 if (getSettings() == null) { 971 return CmsLocaleManager.getDefaultLocale(); 972 } 973 return getSettings().getLocale(); 974 } 975 976 /** 977 * Returns the localized messages object for the current user.<p> 978 * 979 * @return the localized messages object for the current user 980 */ 981 public CmsMessages getMessages() { 982 983 return m_messages; 984 } 985 986 /** 987 * Returns the stream this shell writes its regular messages to.<p> 988 * 989 * @return the stream this shell writes its regular messages to 990 */ 991 public PrintStream getOut() { 992 993 return m_out; 994 } 995 996 /** 997 * Gets the prompt.<p> 998 * 999 * @return the prompt 1000 */ 1001 public String getPrompt() { 1002 1003 String prompt = m_prompt; 1004 try { 1005 prompt = CmsStringUtil.substitute(prompt, "${user}", m_cms.getRequestContext().getCurrentUser().getName()); 1006 prompt = CmsStringUtil.substitute(prompt, "${siteroot}", m_cms.getRequestContext().getSiteRoot()); 1007 prompt = CmsStringUtil.substitute( 1008 prompt, 1009 "${project}", 1010 m_cms.getRequestContext().getCurrentProject().getName()); 1011 prompt = CmsStringUtil.substitute(prompt, "${uri}", m_cms.getRequestContext().getUri()); 1012 } catch (Throwable t) { 1013 // ignore 1014 } 1015 return prompt; 1016 } 1017 1018 /** 1019 * Obtain the additional settings related to the current user. 1020 * 1021 * @return the additional settings related to the current user. 1022 */ 1023 public CmsUserSettings getSettings() { 1024 1025 return m_settings; 1026 } 1027 1028 /** 1029 * Returns true if echo mode is on.<p> 1030 * 1031 * @return true if echo mode is on 1032 */ 1033 public boolean hasEcho() { 1034 1035 return m_echo; 1036 } 1037 1038 /** 1039 * Checks whether a report error occurred during execution of the last command.<p> 1040 * 1041 * @return true if a report error occurred 1042 */ 1043 public boolean hasReportError() { 1044 1045 return m_hasReportError; 1046 } 1047 1048 /** 1049 * Initializes the CmsShell.<p> 1050 * 1051 * @param additionalShellCommands optional object for additional shell commands, or null 1052 * @param out stream to write the regular output messages to 1053 * @param err stream to write the error messages output to 1054 */ 1055 public void initShell(I_CmsShellCommands additionalShellCommands, PrintStream out, PrintStream err) { 1056 1057 // set the output streams 1058 m_out = out; 1059 m_err = err; 1060 1061 // initialize the settings of the user 1062 m_settings = initSettings(); 1063 1064 // initialize shell command object 1065 m_shellCommands = new CmsShellCommands(); 1066 m_shellCommands.initShellCmsObject(m_cms, this); 1067 1068 // initialize additional shell command object 1069 if (additionalShellCommands != null) { 1070 m_additionalShellCommands = additionalShellCommands; 1071 m_additionalShellCommands.initShellCmsObject(m_cms, this); 1072 m_additionalShellCommands.shellStart(); 1073 } else { 1074 m_shellCommands.shellStart(); 1075 } 1076 1077 m_commandObjects = new ArrayList<CmsCommandObject>(); 1078 if (m_additionalShellCommands != null) { 1079 // get all shell callable methods from the additional shell command object 1080 m_commandObjects.add(new CmsCommandObject(m_additionalShellCommands)); 1081 } 1082 // get all shell callable methods from the CmsShellCommands 1083 m_commandObjects.add(new CmsCommandObject(m_shellCommands)); 1084 // get all shell callable methods from the CmsRequestContext 1085 m_commandObjects.add(new CmsCommandObject(m_cms.getRequestContext())); 1086 // get all shell callable methods from the CmsObject 1087 m_commandObjects.add(new CmsCommandObject(m_cms)); 1088 } 1089 1090 /** 1091 * Returns true if exit was called.<p> 1092 * 1093 * @return true if exit was called 1094 */ 1095 public boolean isExitCalled() { 1096 1097 return m_exitCalled; 1098 } 1099 1100 /** 1101 * If <code>true</code> this is an interactive session with a user sitting on a console.<p> 1102 * 1103 * @return <code>true</code> if this is an interactive session with a user sitting on a console 1104 */ 1105 public boolean isInteractive() { 1106 1107 return m_interactive; 1108 } 1109 1110 /** 1111 * Prints the shell prompt.<p> 1112 */ 1113 public void printPrompt() { 1114 1115 String prompt = getPrompt(); 1116 m_out.print(prompt); 1117 m_out.flush(); 1118 } 1119 1120 /** 1121 * Set <code>true</code> if this is an interactive session with a user sitting on a console.<p> 1122 * 1123 * This controls the output of the prompt and some other info that is valuable 1124 * on the console, but not required in an automatic session.<p> 1125 * 1126 * @param interactive if <code>true</code> this is an interactive session with a user sitting on a console 1127 */ 1128 public void setInteractive(boolean interactive) { 1129 1130 m_interactive = interactive; 1131 } 1132 1133 /** 1134 * Sets the locale of the current user.<p> 1135 * 1136 * @param locale the locale to set 1137 * 1138 * @throws CmsException in case the locale of the current user can not be stored 1139 */ 1140 public void setLocale(Locale locale) throws CmsException { 1141 1142 CmsUserSettings settings = getSettings(); 1143 if (settings != null) { 1144 settings.setLocale(locale); 1145 settings.save(m_cms); 1146 m_messages = Messages.get().getBundle(locale); 1147 } 1148 } 1149 1150 /** 1151 * Reads the given stream and executes the commands in this shell.<p> 1152 * 1153 * @param inputStream an input stream from which commands are read 1154 * @deprecated use {@link #execute(InputStream)} instead 1155 */ 1156 @Deprecated 1157 public void start(FileInputStream inputStream) { 1158 1159 setInteractive(true); 1160 execute(inputStream); 1161 } 1162 1163 /** 1164 * Validates the given user and password and checks if the user has the requested role.<p> 1165 * 1166 * @param userName the user name 1167 * @param password the password 1168 * @param requiredRole the required role 1169 * 1170 * @return <code>true</code> if the user is valid 1171 */ 1172 public boolean validateUser(String userName, String password, CmsRole requiredRole) { 1173 1174 boolean result = false; 1175 try { 1176 CmsUser user = m_cms.readUser(userName, password); 1177 result = OpenCms.getRoleManager().hasRole(m_cms, user.getName(), requiredRole); 1178 } catch (CmsException e) { 1179 // nothing to do 1180 } 1181 return result; 1182 } 1183 1184 /** 1185 * Shows the signature of all methods containing the given search String.<p> 1186 * 1187 * @param searchString the String to search for in the methods, if null all methods are shown 1188 */ 1189 protected void help(String searchString) { 1190 1191 String commandList; 1192 boolean foundSomething = false; 1193 m_out.println(); 1194 1195 Iterator<CmsCommandObject> i = m_commandObjects.iterator(); 1196 while (i.hasNext()) { 1197 CmsCommandObject cmdObj = i.next(); 1198 commandList = cmdObj.getMethodHelp(searchString); 1199 if (!CmsStringUtil.isEmpty(commandList)) { 1200 1201 m_out.println( 1202 m_messages.key(Messages.GUI_SHELL_AVAILABLE_METHODS_1, cmdObj.getObject().getClass().getName())); 1203 m_out.println(commandList); 1204 foundSomething = true; 1205 } 1206 } 1207 1208 if (!foundSomething) { 1209 m_out.println(m_messages.key(Messages.GUI_SHELL_MATCH_SEARCHSTRING_1, searchString)); 1210 } 1211 } 1212 1213 /** 1214 * Initializes the internal <code>CmsWorkplaceSettings</code> that contain (amongst other 1215 * information) important information additional information about the current user 1216 * (an instance of {@link CmsUserSettings}).<p> 1217 * 1218 * This step is performed within the <code>CmsShell</code> constructor directly after 1219 * switching to run-level 2 and obtaining the <code>CmsObject</code> for the guest user as 1220 * well as when invoking the CmsShell command <code>login</code>.<p> 1221 * 1222 * @return the user settings for the current user. 1223 */ 1224 protected CmsUserSettings initSettings() { 1225 1226 m_settings = new CmsUserSettings(m_cms); 1227 return m_settings; 1228 } 1229 1230 /** 1231 * Executes all commands read from the given reader.<p> 1232 * 1233 * @param reader a Reader from which the commands are read 1234 */ 1235 1236 /** 1237 * Sets the echo status.<p> 1238 * 1239 * @param echo the echo status to set 1240 */ 1241 protected void setEcho(boolean echo) { 1242 1243 m_echo = echo; 1244 } 1245 1246 /** 1247 * Sets the current shell prompt.<p> 1248 * 1249 * To set the prompt, the following variables are available:<p> 1250 * 1251 * <code>$u</code> the current user name<br> 1252 * <code>$s</code> the current site root<br> 1253 * <code>$p</code> the current project name<p> 1254 * 1255 * @param prompt the prompt to set 1256 */ 1257 protected void setPrompt(String prompt) { 1258 1259 m_prompt = prompt; 1260 } 1261}