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