Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2013-2014 Jeff Nelson, Cinchapi Software Collective * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.cinchapi.concourse.shell; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.text.MessageFormat; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.GroovyShell; import jline.TerminalFactory; import jline.console.ConsoleReader; import jline.console.completer.StringsCompleter; import org.apache.thrift.transport.TTransportException; import org.cinchapi.concourse.Tag; import org.cinchapi.concourse.Concourse; import org.cinchapi.concourse.lang.Criteria; import org.cinchapi.concourse.lang.StartState; import org.cinchapi.concourse.thrift.Operator; import org.cinchapi.concourse.thrift.TSecurityException; import org.cinchapi.concourse.Timestamp; import org.cinchapi.concourse.util.Version; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.clutch.dates.StringToTime; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.primitives.Longs; /** * The main program runner for the ConcourseShell client. ConcourseShell wraps a * connection to a ConcourseServer inside of a {@link GroovyShell}, which allows * for rich interaction with Concourse in a scripting environment. * * @author jnelson */ public final class ConcourseShell { /** * Run the program... * * @param args - see {@link Options} * @throws IOException */ public static void main(String... args) throws IOException { ConsoleReader console = new ConsoleReader(); console.setExpandEvents(false); Options opts = new Options(); JCommander parser = new JCommander(opts, args); parser.setProgramName("concourse-shell"); if (opts.help) { parser.usage(); System.exit(1); } if (Strings.isNullOrEmpty(opts.password)) { opts.password = console.readLine("Password [" + opts.username + "]: ", '*'); } try { Concourse concourse = Concourse.connect(opts.host, opts.port, opts.username, opts.password, opts.environment); final String env = concourse.getServerEnvironment(); CommandLine.displayWelcomeBanner(); Binding binding = new Binding(); GroovyShell shell = new GroovyShell(binding); Stopwatch watch = Stopwatch.createUnstarted(); console.println("Client Version " + Version.getVersion(ConcourseShell.class)); console.println("Server Version " + concourse.getServerVersion()); console.println(""); console.println("Connected to the '" + env + "' environment."); console.println(""); console.println("Type HELP for help."); console.println("Type EXIT to quit."); console.println("Use TAB for completion."); console.println(""); console.setPrompt(MessageFormat.format("[{0}/cash]$ ", env)); console.addCompleter(new StringsCompleter(getAccessibleApiMethodsUsingShortSyntax())); final List<String> methods = Lists.newArrayList(getAccessibleApiMethods()); String line; while ((line = console.readLine().trim()) != null) { line = SyntaxTools.handleShortSyntax(line, methods); binding.setVariable("concourse", concourse); binding.setVariable("eq", Operator.EQUALS); binding.setVariable("ne", Operator.NOT_EQUALS); binding.setVariable("gt", Operator.GREATER_THAN); binding.setVariable("gte", Operator.GREATER_THAN_OR_EQUALS); binding.setVariable("lt", Operator.LESS_THAN); binding.setVariable("lte", Operator.LESS_THAN_OR_EQUALS); binding.setVariable("bw", Operator.BETWEEN); binding.setVariable("regex", Operator.REGEX); binding.setVariable("nregex", Operator.NOT_REGEX); binding.setVariable("lnk2", Operator.LINKS_TO); binding.setVariable("date", STRING_TO_TIME); binding.setVariable("time", STRING_TO_TIME); binding.setVariable("where", WHERE); binding.setVariable("tag", STRING_TO_TAG); if (line.equalsIgnoreCase("exit")) { concourse.exit(); System.exit(0); } else if (line.equalsIgnoreCase("help") || line.equalsIgnoreCase("man")) { Process p = Runtime.getRuntime() .exec(new String[] { "sh", "-c", "echo \"" + HELP_TEXT + "\" | less > /dev/tty" }); p.waitFor(); } else if (containsBannedCharSequence(line)) { System.err.println( "Cannot complete command because " + "it contains an illegal character sequence."); } else if (Strings.isNullOrEmpty(line)) { // CON-170 continue; } else { watch.reset().start(); Object value = null; try { value = shell.evaluate(line, "ConcourseShell"); watch.stop(); if (value != null) { System.out.println( "Returned '" + value + "' in " + watch.elapsed(TimeUnit.MILLISECONDS) + " ms"); } else { System.out.println("Completed in " + watch.elapsed(TimeUnit.MILLISECONDS) + " ms"); } } catch (Exception e) { if (e.getCause() instanceof TTransportException) { die(e.getMessage()); } else if (e.getCause() instanceof TSecurityException) { die("A security change has occurred and your " + "session cannot continue"); } else { System.err.print("ERROR: " + e.getMessage()); } } } System.out.print("\n"); } } catch (Exception e) { if (e.getCause() instanceof TTransportException) { die("Unable to connect to " + opts.username + "@" + opts.host + ":" + opts.port + " with the specified password"); } else if (e.getCause() instanceof TSecurityException) { die("Invalid username/password combination."); } else { die(e.getMessage()); } } finally { try { TerminalFactory.get().restore(); } catch (Exception e) { die(e.getMessage()); } } } /** * Return a sorted array that contains all the accessible API methods. * * @return the accessible API methods */ protected static String[] getAccessibleApiMethods() { if (ACCESSIBLE_API_METHODS == null) { Set<String> banned = Sets.newHashSet("equals", "getClass", "hashCode", "notify", "notifyAll", "toString", "wait", "exit"); Set<String> methods = Sets.newTreeSet(); for (Method method : Concourse.class.getMethods()) { if (!Modifier.isStatic(method.getModifiers()) && !banned.contains(method.getName())) { // NOTE: Even though the StringCompleter strips the // "concourse." from these method names, we must add it here // so that we can properly handle short syntax in // SyntaxTools#handleShortSyntax methods.add(MessageFormat.format("concourse.{0}", method.getName())); } } ACCESSIBLE_API_METHODS = methods.toArray(new String[methods.size()]); } return ACCESSIBLE_API_METHODS; } /** * Return {@code true} if {@code string} contains at last one of the * {@link #BANNED_CHAR_SEQUENCES} strings. * * @param string * @return {@code true} if string contains a banned character sequence */ private static boolean containsBannedCharSequence(String string) { for (String charSequence : BANNED_CHAR_SEQUENCES) { if (string.contains(charSequence)) { return true; } } return false; } /** * Print {@code message} to stderr and exit with a non-zero status. * * @param message */ private static void die(String message) { System.err.println("ERROR: " + message); System.exit(127); } /** * Return a sorted array that contains all the accessible API methods using * short syntax. * * @return the accessible API methods using short syntax */ private static String[] getAccessibleApiMethodsUsingShortSyntax() { Set<String> methods = Sets.newTreeSet(); for (String method : getAccessibleApiMethods()) { methods.add(method.replace("concourse.", "")); } return methods.toArray(new String[methods.size()]); } /** * A list of char sequences that we must ban for security and other * miscellaneous purposes. */ private static List<String> BANNED_CHAR_SEQUENCES = Lists.newArrayList("concourse.exit()", "concourse.username", "concourse.password", "concourse.client"); /** * A cache of the API methods that are accessible in CaSH. */ private static String[] ACCESSIBLE_API_METHODS = null; /** * The text that is displayed when the user requests HELP. */ private static String HELP_TEXT = ""; static { try { BufferedReader reader = new BufferedReader( new InputStreamReader(ConcourseShell.class.getResourceAsStream("/man"))); String line; while ((line = reader.readLine()) != null) { line = line.replaceAll("\"", "\\\\\""); HELP_TEXT += line + System.getProperty("line.separator"); } HELP_TEXT = HELP_TEXT.trim(); reader.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * A closure that converts a string value to a tag. */ private static Closure<Tag> STRING_TO_TAG = new Closure<Tag>(null) { private static final long serialVersionUID = 1L; @Override public Tag call(Object arg) { return Tag.create(arg.toString()); } }; /** * A closure that converts a string description to a timestamp. */ private static Closure<Timestamp> STRING_TO_TIME = new Closure<Timestamp>(null) { private static final long serialVersionUID = 1L; @Override public Timestamp call(Object arg) { if (Longs.tryParse(arg.toString()) != null) { // We should assume that the timestamp is in microseconds since // that is the output format used in ConcourseShell return Timestamp.fromMicros(Long.parseLong(arg.toString())); } else { return Timestamp.fromJoda(StringToTime.parseDateTime(arg.toString())); } } }; /** * A closure that returns a nwe CriteriaBuilder object. */ private static Closure<StartState> WHERE = new Closure<StartState>(null) { private static final long serialVersionUID = 1L; @Override public StartState call() { return Criteria.where(); } }; /** * The options that can be passed to the main method of this script. * * @author jnelson */ private static class Options { @Parameter(names = { "-h", "--host" }, description = "The hostname where the Concourse Server is located") public String host = "localhost"; @Parameter(names = { "-p", "--port" }, description = "The port on which the Concourse Server is listening") public int port = 1717; @Parameter(names = { "-u", "--username" }, description = "The username with which to connect") public String username = "admin"; @Parameter(names = "--password", description = "The password", password = false, hidden = true) public String password; @Parameter(names = { "-e", "--environment" }, description = "The environment of the Concourse Server to use") public String environment = ""; @Parameter(names = "--help", help = true, hidden = true) public boolean help; } }