Java tutorial
/* Copyright (c) 2013 OpenPlans. All rights reserved. * This code is licensed under the BSD New License, available at the root * application directory. */ package org.geogit.cli; import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.text.NumberFormat; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.ServiceLoader; import java.util.TreeSet; import javax.annotation.Nullable; import jline.console.ConsoleReader; import jline.console.CursorBuffer; import org.geogit.api.DefaultPlatform; import org.geogit.api.GeoGIT; import org.geogit.api.GlobalInjectorBuilder; import org.geogit.api.Platform; import org.geogit.api.plumbing.ResolveGeogitDir; import org.geogit.api.porcelain.ConfigException; import org.geogit.api.porcelain.ConfigGet; import org.geotools.util.DefaultProgressListener; import org.opengis.util.ProgressListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.bridge.SLF4JBridgeHandler; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterException; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.Files; import com.google.common.io.InputSupplier; import com.google.common.io.Resources; import com.google.inject.Binding; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; //import org.python.core.exceptions; /** * Command Line Interface for geogit. * <p> * Looks up and executes {@link CLICommand} implementations provided by any {@link Guice} * {@link Module} that implements {@link CLIModule} declared in any classpath's * {@code META-INF/services/com.google.inject.Module} file. */ public class GeogitCLI { private static final Logger LOGGER = LoggerFactory.getLogger(GeogitCLI.class); private boolean loggingConfigured; private Injector commandsInjector; private Injector geogitInjector; private Platform platform; private GeoGIT geogit; private ConsoleReader consoleReader; private DefaultProgressListener progressListener; /** * Construct a GeogitCLI with the given console reader. * * @param consoleReader */ public GeogitCLI(final ConsoleReader consoleReader) { this.consoleReader = consoleReader; this.platform = new DefaultPlatform(); GlobalInjectorBuilder.builder = new CLIInjectorBuilder(); Iterable<CLIModule> plugins = ServiceLoader.load(CLIModule.class); commandsInjector = Guice.createInjector(plugins); } /** * @return the platform being used by the geogit command line interface. * @see Platform */ public Platform getPlatform() { return platform; } /** * Sets the platform for the command line interface to use. * * @param platform the platform to use * @see Platform */ public void setPlatform(Platform platform) { checkNotNull(platform); this.platform = platform; } /** * Provides a GeoGIT facade configured for the current repository if inside a repository, * {@code null} otherwise. * <p> * Note the repository is lazily loaded and cached afterwards to simplify the execution of * commands or command options that do not need a live repository. * * @return the GeoGIT facade associated with the current repository, or {@code null} if there's * no repository in the current {@link Platform#pwd() working directory} * @see ResolveGeogitDir */ @Nullable public synchronized GeoGIT getGeogit() { if (geogit == null) { GeoGIT geogit = loadRepository(); setGeogit(geogit); } return geogit; } /** * Gives the command line interface a GeoGIT facade to use. * * @param geogit */ public void setGeogit(@Nullable GeoGIT geogit) { this.geogit = geogit; } /** * Loads the repository _if_ inside a geogit repository and returns a configured {@link GeoGIT} * facade. * * @return a geogit for the current repository or {@code null} if not inside a geogit repository * directory. */ @Nullable private GeoGIT loadRepository() { GeoGIT geogit = newGeoGIT(); if (null != geogit.command(ResolveGeogitDir.class).call()) { geogit.getRepository(); return geogit; } return null; } /** * Constructs a new geogit facade. * * @return the constructed GeoGIT. */ public GeoGIT newGeoGIT() { Injector inj = getGeogitInjector(); return new GeoGIT(inj, platform.pwd()); } /** * @return the Guice injector being used by the command line interface. If one hasn't been made, * it will be created. */ public Injector getGeogitInjector() { if (geogitInjector == null) { geogitInjector = GlobalInjectorBuilder.builder.build(); } return geogitInjector; } /** * Sets the Guice injector for the command line interface to use. * * @param injector the Guice injector to use */ public void setGeogitInjector(@Nullable Injector injector) { geogitInjector = injector; } /** * @return the console reader being used by the command line interface. */ public ConsoleReader getConsole() { return consoleReader; } /** * Closes the GeoGIT facade if it exists. */ public void close() { if (geogit != null) { geogit.close(); geogit = null; } } /** * Entry point for the command line interface. * * @param args */ public static void main(String[] args) { // TODO: revisit in case we need to grafefully shutdown upon CTRL+C // Runtime.getRuntime().addShutdownHook(new Thread() { // @Override // public void run() { // System.err.println("Shutting down..."); // System.err.flush(); // } // }); ConsoleReader consoleReader; try { consoleReader = new ConsoleReader(System.in, System.out); // needed for CTRL+C not to let the console broken consoleReader.getTerminal().setEchoEnabled(true); } catch (Exception e) { throw Throwables.propagate(e); } int exitCode = 0; GeogitCLI cli = new GeogitCLI(consoleReader); cli.processCommand(args); try { cli.close(); } finally { try { consoleReader.getTerminal().restore(); } catch (Exception e) { LOGGER.error(e.getMessage(), e); exitCode = -1; } } System.exit(exitCode); } void tryConfigureLogging() { if (loggingConfigured) { return; } loggingConfigured = true; // instantiate and call ResolveGeogitDir directly to avoid calling getGeogit() and hence get // some logging events before having configured logging final URL geogitDirUrl = new ResolveGeogitDir(getPlatform()).call(); if (geogitDirUrl == null) { // redirect java.util.logging to SLF4J anyways SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); return; } if (!"file".equalsIgnoreCase(geogitDirUrl.getProtocol())) { return; } File geogitdir; try { geogitdir = new File(geogitDirUrl.toURI()); } catch (URISyntaxException e) { throw Throwables.propagate(e); } if (!geogitdir.exists() || !geogitdir.isDirectory()) { return; } final URL loggingFile = getOrCreateLoggingConfigFile(geogitdir); if (loggingFile == null) { return; } try { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); loggerContext.reset(); /* * Set the geogitdir variable for the config file can resolve the default location * ${geogitdir}/log/geogit.log */ loggerContext.putProperty("geogitdir", geogitdir.getAbsolutePath()); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(loggerContext); configurator.doConfigure(loggingFile); // redirect java.util.logging to SLF4J SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); } catch (JoranException e) { LOGGER.error("Error configuring logging from file {}. '{}'", loggingFile, e.getMessage(), e); } } @Nullable private URL getOrCreateLoggingConfigFile(final File geogitdir) { final File logsDir = new File(geogitdir, "log"); if (!logsDir.exists() && !logsDir.mkdir()) { return null; } final File configFile = new File(logsDir, "logback.xml"); if (configFile.exists()) { try { return configFile.toURI().toURL(); } catch (MalformedURLException e) { throw Throwables.propagate(e); } } InputSupplier<InputStream> from; final URL resource = getClass().getResource("logback_default.xml"); try { from = Resources.newInputStreamSupplier(resource); } catch (NullPointerException npe) { LOGGER.warn("Couldn't obtain default logging configuration file"); return null; } try { Files.copy(from, configFile); return configFile.toURI().toURL(); } catch (Exception e) { LOGGER.warn("Error copying logback_default.xml to {}. Using default configuration.", configFile, e); return resource; } } /** * Finds all commands that are bound do the command injector. * * @return a collection of keys, one for each command */ private Collection<Key<?>> findCommands() { Map<Key<?>, Binding<?>> commands = commandsInjector.getBindings(); return commands.keySet(); } public JCommander newCommandParser() { JCommander jc = new JCommander(this); jc.setProgramName("geogit"); for (Key<?> cmd : findCommands()) { Object obj = commandsInjector.getInstance(cmd); if (obj instanceof CLICommand || obj instanceof CLICommandExtension) { jc.addCommand(obj); } } return jc; } /** * Processes a command, catching any exceptions and printing their messages to the console. * * @param args * @return 0 for normal exit, -1 if there was an exception. */ public int processCommand(String... args) { String consoleMessage = null; boolean printError = true; try { execute(args); return 0; } catch (ParameterException paramParseException) { consoleMessage = paramParseException.getMessage() + ". See geogit --help"; } catch (InvalidParameterException paramValidationError) { consoleMessage = paramValidationError.getMessage(); } catch (CommandFailedException cmdFailed) { if (null == cmdFailed.getMessage()) { // this is intentional, see the javadoc for CommandFailedException printError = false; } else { consoleMessage = cmdFailed.getMessage(); } } catch (RuntimeException e) { consoleMessage = String.format("An unhandled error occurred: %s. See the log for more details.", e.getMessage()); LOGGER.error(consoleMessage, e); } catch (IOException ioe) { // can't write to the console, see the javadocs for CLICommand.run(). LOGGER.error( "An IOException was caught, should only happen if an error occurred while writing to the console", ioe); } if (printError) { try { consoleReader.println(Optional.fromNullable(consoleMessage).or("Unknown Error")); consoleReader.flush(); } catch (IOException e) { LOGGER.error("Error writing to the console. Original error: {}", consoleMessage, e); } } return -1; } /** * Executes a command. * * @param args * @throws exceptions thrown by the executed commands. */ public void execute(String... args) throws ParameterException, CommandFailedException, IOException { tryConfigureLogging(); JCommander mainCommander = newCommandParser(); if (null == args || args.length == 0) { printShortCommandList(mainCommander); return; } { args = unalias(args); final String commandName = args[0]; JCommander commandParser = mainCommander.getCommands().get(commandName); if (commandParser == null) { consoleReader.println(args[0] + " is not a geogit command. See geogit --help."); // check for similar commands Map<String, JCommander> candidates = spellCheck(mainCommander.getCommands(), commandName); if (!candidates.isEmpty()) { String msg = candidates.size() == 1 ? "Did you mean this?" : "Did you mean one of these?"; consoleReader.println(); consoleReader.println(msg); for (String name : candidates.keySet()) { consoleReader.println("\t" + name); } } consoleReader.flush(); return; } Object object = commandParser.getObjects().get(0); if (object instanceof CLICommandExtension) { args = Arrays.asList(args).subList(1, args.length).toArray(new String[args.length - 1]); mainCommander = ((CLICommandExtension) object).getCommandParser(); } } mainCommander.parse(args); final String parsedCommand = mainCommander.getParsedCommand(); if (null == parsedCommand) { if (mainCommander.getObjects().size() == 0) { mainCommander.usage(); } else if (mainCommander.getObjects().get(0) instanceof CLICommandExtension) { CLICommandExtension extension = (CLICommandExtension) mainCommander.getObjects().get(0); extension.getCommandParser().usage(); } else { mainCommander.usage(); } } else { JCommander jCommander = mainCommander.getCommands().get(parsedCommand); List<Object> objects = jCommander.getObjects(); CLICommand cliCommand = (CLICommand) objects.get(0); Class<? extends CLICommand> cmdClass = cliCommand.getClass(); if (cmdClass.isAnnotationPresent(RequiresRepository.class) && cmdClass.getAnnotation(RequiresRepository.class).value()) { String workingDir; Platform platform = getPlatform(); if (platform == null || platform.pwd() == null) { workingDir = "Couln't determine working directory."; } else { workingDir = platform.pwd().getAbsolutePath(); } if (getGeogit() == null) { throw new CommandFailedException("Not in a geogit repository: " + workingDir); } } cliCommand.run(this); getConsole().flush(); } } /** * If the passed arguments contains an alias, it replaces it by the full command corresponding * to that alias and returns anew set of arguments * * IF not, it returns the passed arguments * * @param args * @return */ private String[] unalias(String[] args) { String configParam = "alias." + args[0]; if (geogit == null) { // in case the repo is not initialized yet return args; } try { Optional<String> unaliased = geogit.command(ConfigGet.class).setName(configParam).call(); if (!unaliased.isPresent()) { unaliased = geogit.command(ConfigGet.class).setGlobal(true).setName(configParam).call(); } if (!unaliased.isPresent()) { return args; } Iterable<String> tokens = Splitter.on(" ").split(unaliased.get()); List<String> allArgs = Lists.newArrayList(tokens); allArgs.addAll(Lists.newArrayList(Arrays.copyOfRange(args, 1, args.length))); return allArgs.toArray(new String[0]); } catch (ConfigException e) { return args; } } /** * Return all commands with a command name at a levenshtein distance of less than 3, as * potential candidates for a mistyped command * * @param commands the list of all available commands * @param commandName the command name * @return a map filtered according to distance between command names */ private Map<String, JCommander> spellCheck(Map<String, JCommander> commands, final String commandName) { Map<String, JCommander> candidates = Maps.filterEntries(commands, new Predicate<Map.Entry<String, JCommander>>() { @Override public boolean apply(@Nullable Entry<String, JCommander> entry) { char[] s1 = entry.getKey().toCharArray(); char[] s2 = commandName.toCharArray(); int[] prev = new int[s2.length + 1]; for (int j = 0; j < s2.length + 1; j++) { prev[j] = j; } for (int i = 1; i < s1.length + 1; i++) { int[] curr = new int[s2.length + 1]; curr[0] = i; for (int j = 1; j < s2.length + 1; j++) { int d1 = prev[j] + 1; int d2 = curr[j - 1] + 1; int d3 = prev[j - 1]; if (s1[i - 1] != s2[j - 1]) { d3 += 1; } curr[j] = Math.min(Math.min(d1, d2), d3); } prev = curr; } return prev[s2.length] < 3; } }); return candidates; } /** * This prints out only porcelain commands * * @param mainCommander * * @throws IOException */ public void printShortCommandList(JCommander mainCommander) { TreeSet<String> commandNames = Sets.newTreeSet(); int longestCommandLenght = 0; // do this to ignore aliases for (String name : mainCommander.getCommands().keySet()) { JCommander command = mainCommander.getCommands().get(name); Class<? extends Object> clazz = command.getObjects().get(0).getClass(); String packageName = clazz.getPackage().getName(); if (!packageName.startsWith("org.geogit.cli.plumbing")) { commandNames.add(name); longestCommandLenght = Math.max(longestCommandLenght, name.length()); } } ConsoleReader console = getConsole(); try { console.println("usage: geogit <command> [<args>]"); console.println(); console.println("The most commonly used geogit commands are:"); for (String cmd : commandNames) { console.print(Strings.padEnd(cmd, longestCommandLenght, ' ')); console.print("\t"); console.println(mainCommander.getCommandDescription(cmd)); } console.flush(); } catch (IOException e) { throw Throwables.propagate(e); } } /** * This prints out all commands, including plumbing ones, without description * * @param mainCommander * @throws IOException */ public void printCommandList(JCommander mainCommander) { TreeSet<String> commandNames = Sets.newTreeSet(); int longestCommandLenght = 0; // do this to ignore aliases for (String name : mainCommander.getCommands().keySet()) { commandNames.add(name); longestCommandLenght = Math.max(longestCommandLenght, name.length()); } ConsoleReader console = getConsole(); try { console.println("usage: geogit <command> [<args>]"); console.println(); int i = 0; for (String cmd : commandNames) { console.print(Strings.padEnd(cmd, longestCommandLenght, ' ')); i++; if (i % 3 == 0) { console.println(); } else { console.print("\t"); } } console.flush(); } catch (IOException e) { throw Throwables.propagate(e); } } /** * /** * * @return the ProgressListener for the command line interface. If it doesn't exist, a new one * will be constructed. * @see ProgressListener */ public synchronized ProgressListener getProgressListener() { if (this.progressListener == null) { this.progressListener = new DefaultProgressListener() { private final Platform platform = getPlatform(); private final ConsoleReader console = getConsole(); private final NumberFormat fmt = NumberFormat.getPercentInstance(); private final long delayMillis = 300; // Don't skip the first update private volatile long lastRun = -(delayMillis + 1); @Override public void started() { super.started(); lastRun = -(delayMillis + 1); } public void setDescription(String s) { try { console.println(); console.println(s); console.flush(); } catch (IOException e) { Throwables.propagate(e); } } @Override public void complete() { // avoid double logging if caller missbehaves if (super.isCompleted()) { return; } super.complete(); super.dispose(); try { log(100f); console.println(); console.flush(); } catch (IOException e) { Throwables.propagate(e); } } @Override public void progress(float percent) { super.progress(percent); long currentTimeMillis = platform.currentTimeMillis(); if (percent > 99f || (currentTimeMillis - lastRun) > delayMillis) { lastRun = currentTimeMillis; log(percent); } } private void log(float percent) { CursorBuffer cursorBuffer = console.getCursorBuffer(); cursorBuffer.clear(); String description = getDescription(); if (description != null) { cursorBuffer.write(description); } cursorBuffer.write(fmt.format(percent / 100f)); try { console.redrawLine(); console.flush(); } catch (IOException e) { Throwables.propagate(e); } } }; } return this.progressListener; } }