Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / admin / AppCfg.java
blob3e021229e3c7ffe2fcda8ed1e92bafc3212465ac
2 // Copyright 2008 Google Inc. All Rights Reserved.
4 package com.google.appengine.tools.admin;
6 import com.google.api.client.auth.oauth2.Credential;
7 import com.google.appengine.tools.admin.AppAdmin.LogSeverity;
8 import com.google.appengine.tools.admin.AppAdminFactory.ConnectOptions;
9 import com.google.appengine.tools.admin.ClientLoginServerConnection.ClientAuthFailException;
10 import com.google.appengine.tools.admin.IndexDeleter.DeleteIndexAction;
11 import com.google.appengine.tools.development.DevAppServerMain;
12 import com.google.appengine.tools.info.SupportInfo;
13 import com.google.appengine.tools.info.UpdateCheck;
14 import com.google.appengine.tools.plugins.ActionsAndOptions;
15 import com.google.appengine.tools.plugins.SDKPluginManager;
16 import com.google.appengine.tools.plugins.SDKRuntimePlugin;
17 import com.google.appengine.tools.plugins.SDKRuntimePlugin.ApplicationDirectories;
18 import com.google.appengine.tools.util.Action;
19 import com.google.appengine.tools.util.ClientCookieManager;
20 import com.google.appengine.tools.util.Logging;
21 import com.google.appengine.tools.util.Option;
22 import com.google.appengine.tools.util.Parser;
23 import com.google.appengine.tools.util.Parser.ParseResult;
24 import com.google.appengine.tools.wargen.WarGenerator;
25 import com.google.appengine.tools.wargen.WarGeneratorFactory;
26 import com.google.apphosting.utils.config.AppEngineConfigException;
27 import com.google.apphosting.utils.config.BackendsXml;
28 import com.google.common.base.Joiner;
29 import com.google.common.collect.ImmutableList;
31 import java.io.BufferedReader;
32 import java.io.ByteArrayInputStream;
33 import java.io.File;
34 import java.io.FileWriter;
35 import java.io.IOException;
36 import java.io.InputStreamReader;
37 import java.io.ObjectInputStream;
38 import java.io.PrintWriter;
39 import java.io.Reader;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.Iterator;
45 import java.util.LinkedList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 import java.util.TreeSet;
50 import java.util.prefs.Preferences;
52 /**
53 * The command-line SDK tool for administration of App Engine applications.
56 public class AppCfg {
58 public static final String USE_JAVA7_SYSTEM_PROP ="com.google.apphosting.runtime.use_java7";
60 private static final String EXTERNAL_RESOURCE_DIR_ARG =
61 DevAppServerMain.EXTERNAL_RESOURCE_DIR_ARG;
62 private static final String GENERATE_WAR_ARG =
63 DevAppServerMain.GENERATE_WAR_ARG;
64 private static final String GENERATED_WAR_DIR_ARG =
65 DevAppServerMain.GENERATED_WAR_DIR_ARG;
67 private final ConnectOptions connectOptions;
68 private String externalResourceDir;
69 private boolean generateWar = false;
70 private String generatedWarDir;
71 private AppCfgAction action;
72 private String applicationDirectory;
73 private AppAdmin admin;
74 private boolean passin;
75 private boolean doBatch = true;
76 private boolean doJarSplitting = false;
77 private Set<String> jarSplittingExcludeSuffixes = null;
78 private boolean disablePrompt = false;
79 private File logFile = null;
80 private String compileEncoding = null;
81 private final String prefsEmail;
82 private LoginReader loginReader = null;
83 private String overrideAppId;
84 private String overrideAppVersion;
85 private boolean oauth2 = false;
86 private boolean useCookies = true;
88 public static void main(String[] args) {
89 Logging.initializeLogging();
90 new AppCfg(args);
93 protected AppCfg(String[] cmdLineArgs) {
94 this(new AppAdminFactory(), cmdLineArgs);
97 public AppCfg(AppAdminFactory factory, String[] cmdLineArgs) {
98 connectOptions = new ConnectOptions();
99 Parser parser = new Parser();
101 PrintWriter logWriter;
103 try {
104 logFile = File.createTempFile("appcfg", ".log");
105 logWriter = new PrintWriter(new FileWriter(logFile), true);
106 } catch (IOException e) {
107 throw new RuntimeException("Unable to enable logging.", e);
110 String prefsEmail = null;
112 try {
113 ParseResult result =
114 parser.parseArgs(actionsAndOptions.actions, actionsAndOptions.options, cmdLineArgs);
115 action = (AppCfgAction) result.getAction();
116 try {
117 result.applyArgs();
118 } catch (IllegalArgumentException e) {
119 e.printStackTrace(logWriter);
120 System.out.println("Bad argument: " + e.getMessage());
121 System.out.println(action.getHelpString());
122 System.exit(1);
124 if (System.getProperty("http.proxyHost") != null &&
125 System.getProperty("https.proxyHost") == null) {
126 System.setProperty("https.proxyHost",
127 System.getProperty("http.proxyHost"));
128 if (System.getProperty("http.proxyPort") != null &&
129 System.getProperty("https.proxyPort") == null) {
130 System.setProperty("https.proxyPort",
131 System.getProperty("http.proxyPort"));
135 Application app = null;
136 if (applicationDirectory != null) {
137 File appDirectoryFile = new File(applicationDirectory);
138 validateWarPath(appDirectoryFile);
140 UpdateCheck updateCheck = new UpdateCheck(connectOptions.getServer(), appDirectoryFile,
141 connectOptions.getSecure());
142 updateCheck.maybePrintNagScreen(System.out);
143 updateCheck.checkJavaVersion(System.out);
145 if (oauth2) {
146 authorizeOauth2(connectOptions);
147 } else {
148 prefsEmail = loadCookies(connectOptions);
151 factory.setBatchMode(doBatch);
153 factory.setJarSplittingEnabled(doJarSplitting);
154 if (jarSplittingExcludeSuffixes != null) {
155 factory.setJarSplittingExcludes(jarSplittingExcludeSuffixes);
157 if (compileEncoding != null) {
158 factory.setCompileEncoding(compileEncoding);
160 System.out.println("Reading application configuration data...");
161 app = Application.readApplication(applicationDirectory, overrideAppId,
162 overrideAppVersion);
163 if (externalResourceDir != null) {
164 app.setExternalResourceDir(externalResourceDir);
167 app.setListener(new UpdateListener() {
168 public void onProgress(UpdateProgressEvent event) {
169 System.out.println(event.getPercentageComplete() + "% " + event.getMessage());
172 public void onSuccess(UpdateSuccessEvent event) {
173 System.out.println("Operation complete.");
176 public void onFailure(UpdateFailureEvent event) {
177 System.out.println(event.getFailureMessage());
181 admin = factory.createAppAdmin(connectOptions, app, logWriter);
183 System.out.println("Beginning server interaction for " + app.getAppId() + "...");
184 } else {
185 admin = factory.createAppAdmin(connectOptions, null, logWriter);
188 try {
189 action.execute();
190 } catch (AdminException ex) {
191 System.out.println(ex.getMessage());
192 ex.printStackTrace(logWriter);
193 printLogLocation();
194 System.exit(1);
196 System.out.println("Success.");
198 if (app != null) {
199 if (!connectOptions.getRetainUploadDir()) {
200 System.out.println("Cleaning up temporary files...");
201 app.cleanStagingDirectory();
202 } else {
203 File stage = app.getStagingDir();
204 if (stage == null) {
205 System.out.println("Temporary staging directory was not needed, and not created");
206 } else {
207 System.out.println("Temporary staging directory left in " + stage.getCanonicalPath());
212 } catch (IllegalArgumentException e) {
213 e.printStackTrace(logWriter);
214 System.out.println("Bad argument: " + e.getMessage());
215 printHelp();
216 System.exit(1);
217 } catch (AppEngineConfigException e) {
218 e.printStackTrace(logWriter);
219 System.out.println("Bad configuration: " + e.getMessage());
220 if (e.getCause() != null) {
221 System.out.println(" Caused by: " + e.getCause().getMessage());
223 printLogLocation();
224 System.exit(1);
225 } catch (Exception e) {
226 System.out.println("Encountered a problem: " + e.getMessage());
227 e.printStackTrace(logWriter);
228 printLogLocation();
229 System.exit(1);
231 this.prefsEmail = prefsEmail;
235 * Prints a uniform message to direct the user to the given logfile for
236 * more information.
238 private void printLogLocation() {
239 if (logFile != null) {
240 System.out.println("Please see the logs [" + logFile.getAbsolutePath() +
241 "] for further information.");
245 private String loadCookies(final ConnectOptions options) {
246 Preferences prefs = Preferences.userNodeForPackage(ServerConnection.class);
247 String prefsEmail = prefs.get("email", null);
249 if (options.getUsePersistedCredentials() && prefsEmail != null) {
250 ClientCookieManager cookies = null;
251 byte[] serializedCookies = prefs.getByteArray("cookies", null);
252 if (serializedCookies != null) {
253 try {
254 cookies = (ClientCookieManager)
255 new ObjectInputStream(
256 new ByteArrayInputStream(serializedCookies)).readObject();
257 } catch (ClassNotFoundException ex) {
258 } catch (IOException ex) {
262 if (options.getUserId() == null ||
263 prefsEmail.equals(options.getUserId())) {
264 options.setCookies(cookies);
268 options.setPasswordPrompt(new AppAdminFactory.PasswordPrompt() {
269 public String getPassword() {
270 doPrompt();
271 options.setUserId(loginReader.getUsername());
272 return loginReader.getPassword();
275 return prefsEmail;
279 * Tries to get an OAuth2 access token and set it in the ConnectOptions.
280 * It exists with exit code 1 in case no token could be obtained.
282 private void authorizeOauth2(final ConnectOptions options){
283 OAuth2Native client = new OAuth2Native(useCookies);
284 Credential credential = client.authorize();
285 if (credential != null && credential.getAccessToken() != null) {
286 options.setOauthToken(credential.getAccessToken());
287 } else {
288 System.out.println("Either the code entered is invalid or the token was revoked.");
289 System.exit(1);
294 * Helper function for generating a war directory based on an app.yaml file located in an external
295 * resource directory. First the command line arguments are checked to ensure that they are
296 * appropriate for war generation. If there is a problem then a {@link RuntimeException} is
297 * thrown. Otherwise a war directory is generated and its path is returned, and a success
298 * message is written to standard out.
300 * @return The path of the generated war directory.
302 private String validateArgsAndGenerateWar() {
303 if (externalResourceDir == null) {
304 throw new IllegalArgumentException("When generating a war directory --"
305 + EXTERNAL_RESOURCE_DIR_ARG + " must also be specified.");
307 File externalResourceDirectory = new File(externalResourceDir);
308 if (!externalResourceDirectory.isDirectory()) {
309 throw new IllegalArgumentException(externalResourceDir + " is not an existing directory.");
311 File appYamlFile = new File(externalResourceDirectory, WarGenerator.APP_YAML);
312 if (!appYamlFile.isFile()) {
313 throw new IllegalArgumentException(appYamlFile.getPath() + " not found.");
315 File destination = (generatedWarDir == null ? null : new File(generatedWarDir));
316 try {
317 WarGenerator warGen =
318 WarGeneratorFactory.newWarGenerator(externalResourceDirectory, destination);
319 String warDir = warGen.generateWarDirectory().getPath();
320 System.out.println("Successfully generated war directory at " + warDir);
321 return warDir;
322 } catch (IOException e) {
323 throw new RuntimeException("Unable to generate a war directory.", e);
327 private void doPrompt() {
329 if (disablePrompt) {
330 System.out.println("Your authentication credentials can't be found and may have expired.\n" +
331 "Please run appcfg directly from the command line to re-establish your credentials.");
332 System.exit(1);
335 getLoginReader().doPrompt();
339 private LoginReader getLoginReader() {
340 if (loginReader == null) {
341 loginReader = LoginReaderFactory.createLoginReader(connectOptions, passin, prefsEmail);
343 return loginReader;
346 private static final List<String> generalOptionNamesInHelpOrder =
347 ImmutableList.of(
348 "server",
349 "email",
350 "host",
351 "proxy",
352 "proxy_https",
353 "no_cookies",
354 "sdk_root",
355 "passin",
356 "insecure",
357 "application",
358 "version",
359 "oauth2",
360 "use_java7");
362 private static final List<String> optionNamesInHelpOrder =
363 ImmutableList.<String>builder().addAll(generalOptionNamesInHelpOrder).add(
364 "enable_jar_splitting",
365 "jar_splitting_excludes",
366 "retain_upload_dir",
367 "compile_encoding",
368 "num_days",
369 "severity",
370 "append",
371 "num_runs",
372 "force"
373 ).build();
375 private static final List<String> actionNamesInHelpOrder =
376 ImmutableList.of(
377 "help",
378 "download_app",
379 "request_logs",
380 "rollback",
381 "update",
382 "update_indexes",
383 "update_cron",
384 "update_queues",
385 "update_dos",
386 "version",
387 "set_default_version",
388 "cron_info",
389 "resource_limits_info",
390 "vacuum_indexes",
391 "backends list",
392 "backends update",
393 "backends rollback",
394 "backends start",
395 "backends stop",
396 "backends delete",
397 "backends configure");
399 private String helpText = null;
400 private void printHelp() {
401 if (helpText == null) {
402 List<String> helpLines = new LinkedList<String>();
403 helpLines.add("usage: AppCfg [options] <action> [<app-dir>] [<argument>]");
404 helpLines.add("");
405 helpLines.add("Action must be one of:");
406 for (String actionName : actionsAndOptions.actionNames) {
407 Action action = actionsAndOptions.getAction(actionName);
408 if (action != null) {
409 helpLines.add(" " + actionName + ": " + action.getShortDescription());
412 helpLines.add("Use 'help <action>' for a detailed description.");
413 helpLines.add("");
414 helpLines.add("options:");
415 for (String optionName : actionsAndOptions.optionNames) {
416 Option option = actionsAndOptions.getOption(optionName);
417 helpLines.addAll(option.getHelpLines());
419 helpText = Joiner.on("\n").join(helpLines);
421 System.out.println(helpText);
422 System.out.println();
425 private final List<Option> builtInOptions = Arrays.asList(
427 new Option("h", "help", true) {
428 @Override
429 public List<String> getHelpLines() {
430 return ImmutableList.<String>of(
431 " -h, --help Show the help message and exit.");
433 @Override
434 public void apply() {
435 printHelp();
436 System.exit(1);
440 new Option("s", "server", false) {
441 @Override
442 public List<String> getHelpLines() {
443 return ImmutableList.<String>of(
444 " -s SERVER, --server=SERVER",
445 " The server to connect to.");
447 @Override
448 public void apply() {
449 connectOptions.setServer(getValue());
453 new Option("e", "email", false) {
454 @Override
455 public List<String> getHelpLines() {
456 return ImmutableList.<String>of(
457 " -e EMAIL, --email=EMAIL",
458 " The username to use. Will prompt if omitted.");
460 @Override
461 public void apply() {
462 connectOptions.setUserId(getValue());
466 new Option("H", "host", false) {
467 @Override
468 public List<String> getHelpLines() {
469 return ImmutableList.<String>of(
470 " -H HOST, --host=HOST Overrides the Host header sent with all RPCs.");
472 @Override
473 public void apply() {
474 connectOptions.setHost(getValue());
478 new Option("p", "proxy", false) {
479 @Override
480 public List<String> getHelpLines() {
481 return ImmutableList.<String>of(
482 " -p PROXYHOST[:PORT], --proxy=PROXYHOST[:PORT]",
483 " Proxies requests through the given proxy server.",
484 " If --proxy_https is also set, only HTTP will be",
485 " proxied here, otherwise both HTTP and HTTPS will.");
487 @Override
488 public void apply() {
489 HostPort hostport = new HostPort(getValue());
491 System.setProperty("http.proxyHost", hostport.getHost());
492 if (hostport.hasPort()) {
493 System.setProperty("http.proxyPort", hostport.getPort());
498 new Option(null, "proxy_https", false) {
499 @Override
500 public List<String> getHelpLines() {
501 return ImmutableList.<String>of(
502 " --proxy_https=PROXYHOST[:PORT]",
503 " Proxies HTTPS requests through the given proxy server.");
505 @Override
506 public void apply() {
507 HostPort hostport = new HostPort(getValue());
509 System.setProperty("https.proxyHost", hostport.getHost());
510 if (hostport.hasPort()) {
511 System.setProperty("https.proxyPort", hostport.getPort());
516 new Option(null, "insecure", true) {
517 @Override
518 public List<String> getHelpLines() {
519 return ImmutableList.<String>of(
520 " --insecure Do not use HTTPS to communicate with the Admin Console.");
522 @Override
523 public void apply() {
524 connectOptions.setSecure(false);
528 new Option(null, "no_cookies", true) {
529 @Override
530 public List<String> getHelpLines() {
531 return ImmutableList.<String>of(
533 " --no_cookies Do not save/load access credentials to/from disk.");
535 @Override
536 public void apply() {
537 useCookies = false;
538 connectOptions.setUsePersistedCredentials(false);
542 new Option("f", "force", true) {
543 @Override
544 public List<String> getHelpLines() {
545 return ImmutableList.<String>of(
546 " -f, --force Force deletion of indexes without being prompted.");
548 @Override
549 public void apply(){
550 if (action instanceof VacuumIndexesAction){
551 VacuumIndexesAction viAction = (VacuumIndexesAction) action;
552 viAction.promptUserForEachDelete = false;
557 new Option("a", "append", true) {
558 @Override
559 public List<String> getHelpLines() {
560 return ImmutableList.<String>of(
561 " -a, --append Append to existing file.");
563 @Override
564 public void apply() {
565 RequestLogsAction logsAction = (RequestLogsAction) action;
566 logsAction.append = true;
570 new Option("n", "num_days", false) {
571 @Override
572 public List<String> getHelpLines() {
573 return ImmutableList.<String>of(
574 " -n NUM_DAYS, --num_days=NUM_DAYS",
575 " Number of days worth of log data to get. The cut-off",
576 " point is midnight UTC. Use 0 to get all available",
577 " logs. Default is 1.");
579 @Override
580 public void apply() {
581 if (action instanceof RequestLogsAction) {
582 RequestLogsAction logsAction = (RequestLogsAction) action;
583 try {
584 logsAction.numDays = Integer.parseInt(getValue());
585 } catch (NumberFormatException e) {
586 throw new IllegalArgumentException("num_days must be an integral number.");
588 } else {
589 CronInfoAction croninfoAction = (CronInfoAction) action;
590 croninfoAction.setNumRuns(getValue());
595 new Option(null, "num_runs", false) {
596 @Override
597 public List<String> getHelpLines() {
598 return ImmutableList.<String>of(
599 " -n NUM_RUNS, --num_runs=NUM_RUNS",
600 " Number of scheduled execution times to compute");
602 @Override
603 public void apply() {
604 CronInfoAction croninfoAction = (CronInfoAction) action;
605 croninfoAction.setNumRuns(getValue());
609 new Option(null, "severity", false) {
610 @Override
611 public List<String> getHelpLines() {
612 return ImmutableList.<String>of(
613 " --severity=SEVERITY Severity of app-level log messages to get. The range",
614 " is 0 (DEBUG) through 4 (CRITICAL). If omitted, only",
615 " request logs are returned.");
617 @Override
618 public void apply() {
619 RequestLogsAction logsAction = (RequestLogsAction) action;
620 try {
621 int severity = Integer.parseInt(getValue());
622 int maxSeverity = LogSeverity.values().length;
623 if (severity < 0 || severity > maxSeverity) {
624 throw new IllegalArgumentException("severity must be between 0 and " + maxSeverity);
626 logsAction.severity = severity;
627 } catch (NumberFormatException e) {
628 for (Enum severity : LogSeverity.values()) {
629 if (getValue().equalsIgnoreCase(severity.toString())) {
630 logsAction.severity = severity.ordinal();
631 return;
634 throw new IllegalArgumentException("severity must be an integral "
635 + "number 0-4, or one of DEBUG, INFO, WARN, ERROR, CRITICAL");
640 new Option(null, "sdk_root", false) {
641 @Override
642 public List<String> getHelpLines() {
643 return ImmutableList.<String>of(
644 " --sdk_root=root Overrides where the SDK is located.");
646 @Override
647 public void apply() {
648 connectOptions.setSdkRoot(getValue());
652 new Option(null, "enable_jar_splitting", true) {
653 @Override
654 public List<String> getHelpLines() {
655 return ImmutableList.<String>of(
656 " --enable_jar_splitting",
657 " Split large jar files (> 10M) into smaller fragments.");
659 @Override
660 public void apply() {
661 doJarSplitting = true;
665 new Option(null, "jar_splitting_excludes", false) {
666 @Override
667 public List<String> getHelpLines() {
668 return ImmutableList.<String>of(
669 " --jar_splitting_excludes=SUFFIXES",
670 " When --enable-jar-splitting is set, files that match",
671 " the list of comma separated SUFFIXES will be excluded",
672 " from all jars.");
674 @Override
675 public void apply() {
676 jarSplittingExcludeSuffixes = new HashSet<String>(Arrays.asList(getValue().split(",")));
680 new Option(null, "retain_upload_dir", true) {
681 @Override
682 public List<String> getHelpLines() {
683 return ImmutableList.<String>of(
684 " --retain_upload_dir",
685 " Do not delete temporary (staging) directory used in",
686 " uploading.");
688 @Override
689 public void apply() {
690 connectOptions.setRetainUploadDir(true);
694 new Option(null, "passin", true) {
695 @Override
696 public List<String> getHelpLines() {
697 return ImmutableList.<String>of(
698 " --passin Always read the login password from stdin.");
700 @Override
701 public void apply() {
702 passin = true;
706 new Option(null, "no_batch", true) {
707 @Override
708 public void apply() {
709 doBatch = false;
712 new Option(null, "compile_encoding", false) {
713 @Override
714 public List<String> getHelpLines() {
715 return ImmutableList.<String>of(
716 " --compile_encoding",
717 " The character encoding to use when compiling JSPs.");
719 @Override
720 public void apply() {
721 compileEncoding = getValue();
725 new Option(null, "disable_prompt", true) {
726 @Override
727 public void apply() {
728 disablePrompt = true;
732 new Option("A", "application", false) {
733 @Override
734 public List<String> getHelpLines() {
735 return ImmutableList.<String>of(
736 " -A APP_ID, --application=APP_ID",
737 " Override application id from appengine-web.xml or app.yaml");
739 @Override
740 public void apply() {
741 overrideAppId = getValue();
745 new Option("V", "version" , false) {
746 @Override
747 public List<String> getHelpLines() {
748 return ImmutableList.<String>of(
749 " -V VERSION, --version=VERSION",
750 " Override (major) version from appengine-web.xml " +
751 "or app.yaml");
753 @Override
754 public void apply() {
755 overrideAppVersion = getValue();
759 new Option(null, "oauth2", true) {
760 @Override
761 public List<String> getHelpLines() {
762 return ImmutableList.<String>of(
763 " --oauth2 Use OAuth2 instead of password auth.");
765 @Override
766 public void apply() {
767 oauth2 = true;
771 new Option(null, "use_java7", true) {
772 @Override
773 public List<String> getHelpLines() {
774 return ImmutableList.<String>of(
775 " --use_java7 Use the App Engine Java 7 runtime for this app.");
777 @Override
778 public void apply() {
779 System.setProperty(USE_JAVA7_SYSTEM_PROP, "true");
783 new Option(null, EXTERNAL_RESOURCE_DIR_ARG, false) {
784 @Override
785 public void apply() {
786 externalResourceDir = getValue();
790 new Option(null, GENERATE_WAR_ARG, true) {
791 @Override
792 public void apply() {
793 generateWar = true;
797 new Option(null, GENERATED_WAR_DIR_ARG, false) {
798 @Override
799 public void apply() {
800 generateWar = true;
801 generatedWarDir = getValue();
805 private final List<Action> builtInActions = Arrays.<Action>asList(
806 new UpdateAction(),
807 new RequestLogsAction(),
808 new RollbackAction(),
809 new UpdateIndexesAction(),
810 new UpdateCronAction(),
811 new UpdateDosAction(),
812 new UpdateQueueAction(),
813 new CronInfoAction(),
814 new VacuumIndexesAction(),
815 new HelpAction(),
816 new DownloadAppAction(),
817 new VersionAction(),
818 new SetDefaultVersionAction(),
819 new ResourceLimitsInfoAction(),
820 new BackendsListAction(),
821 new BackendsRollbackAction(),
822 new BackendsUpdateAction(),
823 new BackendsStartAction(),
824 new BackendsStopAction(),
825 new BackendsDeleteAction(),
826 new BackendsConfigureAction(),
827 new BackendsAction()
830 private Map<String, Option> builtInOptionMap;
832 private List<Option> builtInOptions(String... optionNames) {
833 if (builtInOptionMap == null) {
834 builtInOptionMap = new HashMap<String, Option>(builtInOptions.size());
835 for (Option option : builtInOptions){
836 builtInOptionMap.put(option.getLongName(), option);
839 List<Option> options = new LinkedList<Option>();
840 for (String name : optionNames) {
841 Option option = builtInOptionMap.get(name);
842 if (option != null) {
843 options.add(option);
846 return options;
849 private final ActionsAndOptions actionsAndOptions = buildActionsAndOptions();
851 private ActionsAndOptions buildActionsAndOptions() {
852 ActionsAndOptions actionsAndOptions = getBuiltInActionsAndOptions();
853 for (SDKRuntimePlugin runtimePlugin : SDKPluginManager.findAllRuntimePlugins()) {
854 runtimePlugin.customizeAppCfgActionsAndOptions(actionsAndOptions);
856 return actionsAndOptions;
860 * Builds the collection of built-in Actions and Options.
862 private ActionsAndOptions getBuiltInActionsAndOptions() {
863 ActionsAndOptions actionsAndOptions = new ActionsAndOptions();
864 actionsAndOptions.actions = builtInActions;
865 actionsAndOptions.actionNames = actionNamesInHelpOrder;
866 actionsAndOptions.options = builtInOptions;
867 actionsAndOptions.optionNames = optionNamesInHelpOrder;
868 actionsAndOptions.generalOptionNames = generalOptionNamesInHelpOrder;
869 return actionsAndOptions;
872 abstract class AppCfgAction extends Action {
874 AppCfgAction(String... names) {
875 this(null, names);
878 AppCfgAction(List<Option> options, String... names) {
879 super(options, names);
882 @Override
883 protected void setArgs(List<String> args) {
884 super.setArgs(args);
887 @Override
888 public void apply() {
889 if (generateWar) {
890 applicationDirectory = validateArgsAndGenerateWar();
891 List<String> args = getArgs();
892 List<String> newArgs = new ArrayList<String>(args.size() + 1);
893 newArgs.add(applicationDirectory);
894 newArgs.addAll(args);
895 setArgs(newArgs);
896 } else {
897 if (getArgs().size() < 1) {
898 throw new IllegalArgumentException("Expected the application directory"
899 + " as an argument after the action name.");
901 applicationDirectory = getArgs().get(0);
903 SDKRuntimePlugin runtimePlugin = SDKPluginManager.findRuntimePlugin(
904 new File(applicationDirectory));
905 if (runtimePlugin != null) {
906 try {
907 ApplicationDirectories appDirs = runtimePlugin.generateApplicationDirectories(
908 new File(applicationDirectory));
909 applicationDirectory = appDirs.getWarDir().getPath();
910 getArgs().set(0, applicationDirectory);
911 externalResourceDir = appDirs.getExternalResourceDir().getPath();
912 } catch (IOException e) {
913 throw new RuntimeException("Unable to generate the war directory", e);
919 public abstract void execute();
921 @Override
922 protected List<String> getHelpLines() {
923 List<String> helpLines = new LinkedList<String>();
924 helpLines.addAll(getInitialHelpLines());
925 helpLines.add("");
926 helpLines.add("Options:");
927 for (String optionName : actionsAndOptions.generalOptionNames) {
928 Option option = actionsAndOptions.getOption(optionName);
929 if (option != null) {
930 helpLines.addAll(option.getHelpLines());
933 if (extraOptions != null) {
934 for (Option option : extraOptions) {
935 helpLines.addAll(option.getHelpLines());
938 return helpLines;
942 * Returns a list of Strings to be displayed as the initial lines of a help text. Subclasses
943 * should override this method.
944 * <p>
945 * The text returned by this method should describe the base Action without any of its options.
946 * Text describing the options will be added in lines below this text.
948 protected List<String> getInitialHelpLines() {
949 return ImmutableList.of();
954 class UpdateAction extends AppCfgAction {
955 UpdateAction() {
956 super(builtInOptions("enable_jar_splitting", "jar_splitting_excludes", "retain_upload_dir",
957 "compile_encoding"), "update");
958 shortDescription = "Create or update an app version.";
961 @Override
962 public void execute() {
963 admin.update(new AppCfgUpdateListener());
966 @Override
967 protected List<String> getInitialHelpLines() {
968 return ImmutableList.of(
969 "AppCfg [options] update <app-dir>",
971 "Installs a new version of the application onto the server, as the",
972 "default version for end users.");
976 class RequestLogsAction extends AppCfgAction {
977 String outputFile;
978 int numDays = 1;
979 int severity = -1;
980 boolean append = false;
982 RequestLogsAction() {
983 super(builtInOptions("num_days", "severity", "append"), "request_logs");
984 shortDescription = "Write request logs in Apache common log format.";
986 @Override
987 public void apply() {
988 super.apply();
989 if (getArgs().size() != 2) {
990 throw new IllegalArgumentException("Expected the application directory"
991 + " and log file as arguments after the request_logs action name.");
993 outputFile = getArgs().get(1);
995 @Override
996 public void execute() {
997 Reader reader = admin.requestLogs(numDays,
998 severity >= 0 ? LogSeverity.values()[severity] : null);
999 if (reader == null) {
1000 return;
1003 BufferedReader r = new BufferedReader(reader);
1004 PrintWriter writer = null;
1005 try {
1006 if (outputFile.equals("-")) {
1007 writer = new PrintWriter(System.out);
1008 } else {
1009 writer = new PrintWriter(new FileWriter(outputFile, append));
1011 String line = null;
1012 while ((line = r.readLine()) != null) {
1013 writer.println(line);
1015 } catch (IOException e) {
1016 throw new RuntimeException("Failed to read logs: " + e);
1017 } finally {
1018 if (writer != null) {
1019 writer.close();
1021 try {
1022 r.close();
1023 } catch (IOException e) {
1027 @Override
1028 protected List<String> getInitialHelpLines() {
1029 return ImmutableList.of(
1030 "AppCfg [options] request_logs <app-dir> <output-file>",
1032 "Populates the output-file with recent logs from the application.");
1036 class RollbackAction extends AppCfgAction {
1037 RollbackAction() {
1038 super("rollback");
1039 shortDescription = "Rollback an in-progress update.";
1041 @Override
1042 public void execute() {
1043 admin.rollback();
1045 @Override
1046 protected List<String> getInitialHelpLines() {
1047 return ImmutableList.of(
1048 "AppCfg [options] rollback <app-dir>",
1050 "The 'update' command requires a server-side transaction.",
1051 "Use 'rollback' if you experience an error during 'update'",
1052 "and want to begin a new update transaction.");
1056 class UpdateIndexesAction extends AppCfgAction {
1057 UpdateIndexesAction() {
1058 super("update_indexes");
1059 shortDescription = "Update application indexes.";
1061 @Override
1062 public void execute() {
1063 admin.updateIndexes();
1065 @Override
1066 protected List<String> getInitialHelpLines() {
1067 return ImmutableList.of(
1068 "AppCfg [options] update_indexes <app-dir>",
1070 "Updates the datastore indexes for the server to add any in the current",
1071 "application directory. Does not alter the running application version, nor",
1072 "remove any existing indexes.");
1076 class UpdateCronAction extends AppCfgAction {
1077 UpdateCronAction() {
1078 super("update_cron");
1079 shortDescription = "Update application cron jobs.";
1081 @Override
1082 public void execute() {
1083 admin.updateCron();
1084 shortDescription = "Update application cron jobs.";
1086 @Override
1087 protected List<String> getInitialHelpLines() {
1088 return ImmutableList.of(
1089 "AppCfg [options] update_cron <app-dir>",
1091 "Updates the cron jobs for the server. Updates any new, removed or changed",
1092 "cron jobs. Does not otherwise alter the running application version.");
1096 class UpdateDosAction extends AppCfgAction {
1097 UpdateDosAction() {
1098 super("update_dos");
1099 shortDescription = "Update application DoS protection configuration.";
1101 @Override
1102 public void execute() {
1103 admin.updateDos();
1105 @Override
1106 protected List<String> getInitialHelpLines() {
1107 return ImmutableList.of(
1108 "AppCfg [options] update_dos <app-dir>",
1110 "Updates the DoS protection configuration for the server.",
1111 "Does not otherwise alter the running application version.");
1115 class UpdateQueueAction extends AppCfgAction {
1116 UpdateQueueAction() {
1117 super("update_queues");
1118 shortDescription = "Update application task queue definitions.";
1120 @Override
1121 public void execute() {
1122 admin.updateQueues();
1124 @Override
1125 protected List<String> getInitialHelpLines() {
1126 return ImmutableList.of(
1127 "AppCfg [options] " + getNameString() + " <app-dir>",
1129 "Updates any new, removed or changed task queue definitions.",
1130 "Does not otherwise alter the running application version.");
1134 class CronInfoAction extends AppCfgAction {
1135 int numRuns = 5;
1137 CronInfoAction() {
1138 super(builtInOptions("num_runs"), "cron_info");
1139 shortDescription = "Displays times for the next several runs of each cron job.";
1141 @Override
1142 public void execute() {
1143 List<CronEntry> entries = admin.cronInfo();
1144 if (entries.size() == 0) {
1145 System.out.println("No cron jobs defined.");
1146 } else {
1147 System.out.println(entries.size() + " cron entries defined.\n");
1148 for (CronEntry entry : entries) {
1149 System.out.println(entry.toXml());
1150 System.out.println("Next " + numRuns + " execution times:");
1151 Iterator<String> iter = entry.getNextTimesIterator();
1152 for (int i = 0; i < numRuns; i++) {
1153 System.out.println(" " + iter.next());
1155 System.out.println("");
1159 @Override
1160 protected List<String> getInitialHelpLines() {
1161 return ImmutableList.of(
1162 "AppCfg [options] cron_info <app-dir>",
1164 "Displays times for the next several runs of each cron job.");
1166 public void setNumRuns(String numberString) {
1167 try {
1168 numRuns = Integer.parseInt(numberString);
1169 } catch (NumberFormatException e) {
1170 throw new IllegalArgumentException("num_runs must be an integral number.");
1172 if (numRuns < 0) {
1173 throw new IllegalArgumentException("num_runs must be positive.");
1178 class VacuumIndexesAction extends AppCfgAction {
1179 public boolean promptUserForEachDelete = true;
1181 VacuumIndexesAction() {
1182 super(builtInOptions("force"), "vacuum_indexes");
1183 shortDescription = "Delete unused indexes from application.";
1186 @Override
1187 public void execute() {
1188 ConfirmationCallback<IndexDeleter.DeleteIndexAction> callback = null;
1189 if (promptUserForEachDelete) {
1190 callback = new ConfirmationCallback<IndexDeleter.DeleteIndexAction>() {
1191 @Override
1192 public Response confirmAction(DeleteIndexAction action) {
1193 while (true) {
1194 String prompt = "\n" + action.getPrompt() + " (N/y/a): ";
1195 System.out.print(prompt);
1196 System.out.flush();
1197 BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
1198 String response;
1199 try {
1200 response = in.readLine();
1201 } catch (IOException ioe) {
1202 response = null;
1204 response = (null == response ? "" : response.trim().toLowerCase());
1205 if ("y".equals(response)) {
1206 return Response.YES;
1208 if ("n".equals(response) || response.isEmpty()) {
1209 return Response.NO;
1211 if ("a".equals(response)) {
1212 return Response.YES_ALL;
1218 admin.vacuumIndexes(callback, new AppCfgVacuumIndexesListener());
1221 @Override
1222 protected List<String> getInitialHelpLines() {
1223 return ImmutableList.of(
1224 "AppCfg [options] vacuum_indexes <app-dir>",
1226 "Deletes indexes on the server that are not present in the local",
1227 "index configuration file. The user is prompted before each delete.");
1231 class HelpAction extends AppCfgAction {
1232 HelpAction() {
1233 super("help");
1234 shortDescription = "Print help for a specific action.";
1236 @Override
1237 public void apply() {
1238 if (getArgs().isEmpty()) {
1239 printHelp();
1240 } else {
1241 Action foundAction = Parser.lookupAction(actionsAndOptions.actions,
1242 getArgs().toArray(new String[0]), 0);
1243 if (foundAction == null) {
1244 System.out.println("No such command \"" + getArgs().get(0) + "\"\n\n");
1245 printHelp();
1246 } else {
1247 System.out.println(foundAction.getHelpString());
1248 System.out.println();
1251 System.exit(1);
1253 @Override
1254 public void execute() {
1256 @Override
1257 protected List<String> getHelpLines() {
1258 return ImmutableList.of("AppCfg help <command>",
1260 "Prints help about a specific command.",
1261 "");
1265 class DownloadAppAction extends AppCfgAction {
1266 DownloadAppAction() {
1267 super("download_app");
1268 shortDescription = "Download a previously uploaded app version.";
1270 @Override
1271 public void apply() {
1272 if (getArgs().size() != 1) {
1273 throw new IllegalArgumentException("Expected download directory"
1274 + " as an argument after download_app.");
1276 File downloadDir = new File(getArgs().get(0));
1277 if (overrideAppId == null) {
1278 throw new IllegalArgumentException("You must specify an app ID via -A or --application");
1281 if (oauth2) {
1282 authorizeOauth2(connectOptions);
1283 } else {
1284 loadCookies(connectOptions);
1287 AppDownload appDownload =
1288 new AppDownload(ServerConnectionFactory.getServerConnection(connectOptions),
1289 new AppCfgListener("download_app"));
1290 int exitCode = appDownload.download(overrideAppId, overrideAppVersion, downloadDir) ? 0 : 1;
1291 System.exit(exitCode);
1293 @Override
1294 public void execute() {
1296 @Override
1297 protected List<String> getInitialHelpLines() {
1298 return ImmutableList.of(
1299 "AppCfg [options] -A app_id [ -V version ] download_app <out-dir>",
1301 "Download a previously-uploaded app to the specified directory. The app",
1302 "ID is specified by the \"-A\" option. The optional version is specified",
1303 "by the \"-V\" option.");
1307 class VersionAction extends AppCfgAction {
1308 VersionAction() {
1309 super("version");
1310 shortDescription = "Prints version information.";
1312 @Override
1313 public void apply() {
1314 System.out.println(SupportInfo.getVersionString());
1315 System.exit(0);
1317 @Override
1318 public void execute() {
1320 @Override
1321 protected List<String> getHelpLines() {
1322 return ImmutableList.of(
1323 "AppCfg version",
1325 "Prints version information.");
1329 class SetDefaultVersionAction extends AppCfgAction {
1330 SetDefaultVersionAction() {
1331 super("set_default_version");
1332 shortDescription = "Set the default serving version.";
1334 @Override
1335 public void execute() {
1336 admin.setDefaultVersion();
1338 @Override
1339 protected List<String> getInitialHelpLines() {
1340 return ImmutableList.of(
1341 "AppCfg [options] set_default_version <app-dir>",
1343 "Sets the default (serving) version");
1347 class ResourceLimitsInfoAction extends AppCfgAction {
1348 public ResourceLimitsInfoAction() {
1349 super("resource_limits_info");
1350 shortDescription = "Display resource limits.";
1353 @Override
1354 public void execute() {
1355 ResourceLimits resourceLimits = admin.getResourceLimits();
1356 for (String key : new TreeSet<String>(resourceLimits.keySet())) {
1357 System.out.println(key + ": " + resourceLimits.get(key));
1361 @Override
1362 protected List<String> getInitialHelpLines() {
1363 return ImmutableList.of(
1364 "AppCfg [options] resource_limits_info <app-dir>",
1366 "Displays the resource limits available to the app. An app will",
1367 "not update if any of the app's resources are larger than the",
1368 "appropriate resource limit.");
1372 class BackendsListAction extends AppCfgAction {
1373 BackendsListAction() {
1374 super("backends", "list");
1375 shortDescription = "List the currently configured backends.";
1378 @Override
1379 public void execute() {
1380 for (BackendsXml.Entry backend : admin.listBackends()) {
1381 System.out.println(backend.toString());
1385 @Override
1386 protected List<String> getInitialHelpLines() {
1387 return ImmutableList.of(
1388 "AppCfg [options] backends list <app-dir>",
1390 "List the currently configured backends.");
1394 class BackendsRollbackAction extends AppCfgAction {
1395 private String backendName;
1397 BackendsRollbackAction() {
1398 super("backends", "rollback");
1399 shortDescription = "Roll back a previously in-progress update.";
1402 @Override
1403 public void apply() {
1404 super.apply();
1405 if (getArgs().size() < 1 || getArgs().size() > 2) {
1406 throw new IllegalArgumentException("Expected <app-dir> [<backend-name>]");
1407 } else if (getArgs().size() == 2) {
1408 backendName = getArgs().get(1);
1412 @Override
1413 public void execute() {
1414 List<String> backends;
1415 if (backendName != null) {
1416 admin.rollbackBackend(backendName);
1417 } else {
1418 admin.rollbackAllBackends();
1422 @Override
1423 protected List<String> getInitialHelpLines() {
1424 return ImmutableList.of(
1425 "AppCfg [options] backends rollback <app-dir> [<backend-name>]",
1427 "The 'backends update' command requires a server-side transaction.",
1428 "Use 'backends rollback' if you experience an error during 'backends update'",
1429 "and want to begin a new update transaction.");
1433 class BackendsUpdateAction extends AppCfgAction {
1434 private String backendName;
1436 BackendsUpdateAction() {
1437 super("backends", "update");
1438 shortDescription = "Update the specified backend or all backends.";
1441 @Override
1442 public void apply() {
1443 super.apply();
1444 if (getArgs().size() < 1 || getArgs().size() > 2) {
1445 throw new IllegalArgumentException("Expected <app-dir> [<backend-name>]");
1446 } else if (getArgs().size() == 2) {
1447 backendName = getArgs().get(1);
1451 @Override
1452 public void execute() {
1453 List<String> backends;
1454 if (backendName != null) {
1455 admin.updateBackend(backendName, new AppCfgUpdateListener());
1456 } else {
1457 admin.updateAllBackends(new AppCfgUpdateListener());
1461 @Override
1462 protected List<String> getInitialHelpLines() {
1463 return ImmutableList.of(
1464 "AppCfg [options] backends update <app-dir> [<backend-name>]",
1466 "Update the specified backend or all backends.");
1470 class BackendsStartAction extends AppCfgAction {
1471 private String backendName;
1473 BackendsStartAction() {
1474 super("backends", "start");
1475 shortDescription = "Start the specified backend.";
1478 @Override
1479 public void apply() {
1480 super.apply();
1481 if (getArgs().size() != 2) {
1482 throw new IllegalArgumentException("Expected the backend name");
1484 backendName = getArgs().get(1);
1487 @Override
1488 public void execute() {
1489 admin.setBackendState(backendName, BackendsXml.State.START);
1492 @Override
1493 protected List<String> getInitialHelpLines() {
1494 return ImmutableList.of(
1495 "AppCfg [options] backends start <app-dir> <backend>",
1497 "Starts the backend with the specified name.");
1501 class BackendsStopAction extends AppCfgAction {
1502 private String backendName;
1504 BackendsStopAction() {
1505 super("backends", "stop");
1506 shortDescription = "Stop the specified backend.";
1509 @Override
1510 public void apply() {
1511 super.apply();
1512 if (getArgs().size() != 2) {
1513 throw new IllegalArgumentException("Expected the backend name");
1515 backendName = getArgs().get(1);
1517 @Override
1518 public void execute() {
1519 admin.setBackendState(backendName, BackendsXml.State.STOP);
1522 @Override
1523 protected List<String> getInitialHelpLines() {
1524 return ImmutableList.of(
1525 "AppCfg [options] backends stop <app-dir> <backend>",
1527 "Stops the backend with the specified name.");
1531 class BackendsDeleteAction extends AppCfgAction {
1532 private String backendName;
1534 BackendsDeleteAction() {
1535 super("backends", "delete");
1536 shortDescription = "Delete the specified backend.";
1539 @Override
1540 public void apply() {
1541 super.apply();
1542 if (getArgs().size() != 2) {
1543 throw new IllegalArgumentException("Expected the backend name");
1545 backendName = getArgs().get(1);
1547 @Override
1548 public void execute() {
1549 admin.deleteBackend(backendName);
1552 @Override
1553 protected List<String> getInitialHelpLines() {
1554 return ImmutableList.of(
1555 "AppCfg [options] backends delete",
1557 "Deletes the specified backend.");
1561 class BackendsConfigureAction extends AppCfgAction {
1562 private String backendName;
1564 BackendsConfigureAction() {
1565 super("backends", "configure");
1566 shortDescription = "Configure the specified backend.";
1569 @Override
1570 public void apply() {
1571 super.apply();
1572 if (getArgs().size() != 2) {
1573 throw new IllegalArgumentException("Expected the backend name");
1575 backendName = getArgs().get(1);
1577 @Override
1578 public void execute() {
1579 admin.configureBackend(backendName);
1582 @Override
1583 protected List<String> getInitialHelpLines() {
1584 return ImmutableList.of(
1585 "AppCfg [options] backends configure <app-dir> <backend>",
1587 "Updates the configuration of the backend with the specified name, without",
1588 "stopping instances that are currently running. Only valid for certain",
1589 "settings (instances, options: failfast, options: public).");
1594 * This is a catchall for the case where the user enters "appcfg.sh
1595 * backends app-dir sub-command" rather than "appcfg.sh backends
1596 * sub-command app-dir". It was added to maintain compatibility
1597 * with Python. It simply remaps the arguments and dispatches the
1598 * appropriate action.
1600 class BackendsAction extends AppCfgAction {
1601 private AppCfgAction subAction;
1603 BackendsAction() {
1604 super("backends");
1607 @Override
1608 public void apply() {
1609 super.apply();
1610 if (getArgs().size() < 2) {
1611 throw new IllegalArgumentException("Expected backends <app-dir> <sub-command> [...]");
1613 String dir = getArgs().get(0);
1614 String subCommand = getArgs().get(1);
1615 subAction = (AppCfgAction) Parser.lookupAction(actionsAndOptions.actions,
1616 new String[] {"backends", subCommand},
1618 if (subAction instanceof BackendsAction) {
1619 throw new IllegalArgumentException("Unknown backends subcommand.");
1621 List<String> newArgs = new ArrayList<String>();
1622 newArgs.add(dir);
1623 newArgs.addAll(getArgs().subList(2, getArgs().size()));
1624 subAction.setArgs(newArgs);
1625 subAction.apply();
1628 @Override
1629 public void execute() {
1630 subAction.execute();
1633 @Override
1634 protected List<String> getHelpLines() {
1635 return ImmutableList.of(
1636 "AppCfg [options] backends list: List the currently configured backends.",
1637 "AppCfg [options] backends update: Update the specified backend or all backends.",
1638 "AppCfg [options] backends rollback: Roll back a previously in-progress update.",
1639 "AppCfg [options] backends start: Start the specified backend.",
1640 "AppCfg [options] backends stop: Stop the specified backend.",
1641 "AppCfg [options] backends delete: Delete the specified backend.",
1642 "AppCfg [options] backends configure: Configure the specified backend.");
1646 private static class AppCfgListener implements UpdateListener {
1647 private String operationName;
1649 AppCfgListener(String opName){
1650 operationName = opName;
1652 public void onProgress(UpdateProgressEvent event) {
1653 System.out.println(event.getPercentageComplete() + "% " + event.getMessage());
1656 public void onSuccess(UpdateSuccessEvent event) {
1657 String details = event.getDetails();
1658 if (details.length() > 0) {
1659 System.out.println();
1660 System.out.println("Details:");
1661 System.out.println(details);
1664 System.out.println();
1665 System.out.println(operationName + " completed successfully.");
1668 public void onFailure(UpdateFailureEvent event) {
1669 String details = event.getDetails();
1670 if (details.length() > 0) {
1671 System.out.println();
1672 System.out.println("Error Details:");
1673 System.out.println(details);
1676 System.out.println();
1677 String failMsg = event.getFailureMessage();
1678 System.out.println(failMsg);
1679 if (event.getCause() instanceof ClientAuthFailException) {
1680 System.out.println("Consider using the -e EMAIL option if that"
1681 + " email address is incorrect.");
1686 private static class AppCfgUpdateListener extends AppCfgListener {
1687 AppCfgUpdateListener(){
1688 super("Update");
1692 private static class AppCfgVacuumIndexesListener extends AppCfgListener {
1693 AppCfgVacuumIndexesListener(){
1694 super("vacuum_indexes");
1698 private static class HostPort {
1699 private String host;
1700 private String port;
1702 public HostPort(String hostport) {
1703 int colon = hostport.indexOf(':');
1704 host = colon < 0 ? hostport : hostport.substring(0, colon);
1705 port = colon < 0 ? "" : hostport.substring(colon + 1);
1708 public String getHost() {
1709 return host;
1712 public String getPort() {
1713 return port;
1716 public boolean hasPort() {
1717 return port.length() > 0;
1721 private void validateWarPath(File war) {
1722 if (!war.exists()) {
1723 System.out.println("Unable to find the webapp directory " + war);
1724 printHelp();
1725 System.exit(1);
1726 } else if (!war.isDirectory()) {
1727 System.out.println("appcfg only accepts webapp directories, not war files.");
1728 printHelp();
1729 System.exit(1);