/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cinchapi.concourse.server.cli;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.annotation.Nullable;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransportException;
import jline.console.ConsoleReader;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.cinchapi.concourse.server.ConcourseServer;
import com.cinchapi.concourse.server.GlobalState;
import com.cinchapi.concourse.server.management.ConcourseManagementService.Client;
import com.cinchapi.concourse.thrift.AccessToken;
import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;
/**
* A CLI that performs an operation on a {@link ConcourseServerMXBean}. Any CLI
* that operates on a running {@link ConcourseServer} should extend this class.
* <p>
* Unlike children of {@code com.cinchapi.concourse.cli.CommandLineInterface},
* CLIs that extend this class are assumed to live within the Concourse Server
* deployment in the standard directory for CLIs.
* </p>
*
* @author Jeff Nelson
*/
public abstract class ManagementCli {
/**
* The host where the management server is located.
*/
private static String MANAGEMENT_SERVER_HOST = "localhost";
/**
* Handler to the console for interactive I/O.
*/
protected ConsoleReader console;
/**
* The CLI options.
*/
protected Options options;
/**
* The parser that validates the CLI options.
*/
protected JCommander parser;
/**
* {@link AccessToken} access token for management server
*/
protected AccessToken token;
/**
* A socket that is opened to the management server in the {@link #run()}
* method.
*/
@Nullable
private TSocket socket;
@Nullable
private Client client;
/**
* Construct a new instance that is seeded with an object containing options
* metadata. The {@code options} will be parsed by {@link JCommander} to
* configure them appropriately.
*
* @param options
* @param args - these usually come from the main method
*/
public ManagementCli(Options options, String... args) {
try {
this.parser = new JCommander(options, args);
this.options = options;
parser.setProgramName(CaseFormat.UPPER_CAMEL.to(
CaseFormat.LOWER_HYPHEN, this.getClass().getSimpleName()));
if(!isReadyToRun()) {
parser.usage();
System.exit(1);
}
this.console = new ConsoleReader();
console.setExpandEvents(false);
}
catch (ParameterException e) {
die(e.getMessage());
}
catch (IOException e) {
die(e.getMessage());
}
}
/**
* Run the CLI. This method should only be called from the main method.
*/
public final void run() {
try {
socket = new TSocket(MANAGEMENT_SERVER_HOST,
GlobalState.MANAGEMENT_PORT);
socket.open();
client = new Client(new TBinaryProtocol(socket));
if(Strings.isNullOrEmpty(options.password)) {
options.password = console.readLine(
"password for [" + options.username + "]: ", '*');
}
token = client.login(ByteBuffer.wrap(options.username.getBytes()),
ByteBuffer.wrap(options.password.getBytes()));
doTask(client);
exit(0);
}
catch (TTransportException e) {
die("Could not connect to the management server. Please check "
+ "that Concourse Server is running.");
}
catch (Exception e) {
die(e.getMessage());
}
}
/**
* Print {@code message} to stderr and exit with a non-zero status.
*
* @param message
*/
protected void die(String message) {
System.err.println("ERROR: " + message);
exit(2);
}
/**
* Implement a managed task that involves at least one of the operations
* available from {@code client}. This method is called by the main
* {@link #run()} method, so the implementer should place all task logic
* here.
* <p>
* DO NOT call {@link System#exit(int)} with '0' from this method
* </p>
*
* @param client
*/
protected abstract void doTask(Client client);
/**
* Return the host Concourse Server's "home" directory.
*
* @return the host application's home
*/
@Nullable
protected final Path getServerHome() {
String path = System.getProperty("user.app.home"); // this is set by the
// .env script that
// is sourced by
// every server-side
// CLI
return path != null ? Paths.get(path) : null;
}
/**
* Return the original working directory from which the CLI was launched.
* This information is sometimes necessary to properly resolve file paths.
*
* @return the launch directory or {@code null} if the CLI is unable to
* determine its original working directory
*/
@Nullable
protected final String getLaunchDirectory() {
return System.getProperty("user.dir.real"); // this is set by the .env
// script that is sourced by
// every server-side CLI
}
/**
* Return {@code true} if the managed task has sufficient conditions
* to run.
*
* @return {@code true} if the managed task has sufficient conditions
* to run
*/
protected boolean isReadyToRun() {
return !options.help;
}
/**
* Clean up resources and issue a {@link System#exit(int)} with the
* specified {@code status}.
*
* @param status the exit status
*/
private void exit(int status) {
if(client != null) {
try {
client.logout(token);
}
catch (TException e) {
e.printStackTrace();
}
}
if(socket != null) {
socket.close();
}
System.exit(status);
}
}