/******************************************************************************* * Copyright (c) 2010-2015 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.core.gerrit.ssh; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.eclipse.skalli.commons.CollectionUtils; import org.eclipse.skalli.commons.HtmlUtils; import org.eclipse.skalli.commons.JSONUtils; import org.eclipse.skalli.services.gerrit.CommandException; import org.eclipse.skalli.services.gerrit.ConnectionException; import org.eclipse.skalli.services.gerrit.GerritClient; import org.eclipse.skalli.services.gerrit.GerritFeature; import org.eclipse.skalli.services.gerrit.GerritPlugin; import org.eclipse.skalli.services.gerrit.GerritServerConfig; import org.eclipse.skalli.services.gerrit.GerritVersion; import org.eclipse.skalli.services.gerrit.InheritableBoolean; import org.eclipse.skalli.services.gerrit.ProjectOptions; import org.eclipse.skalli.services.gerrit.SubmitType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; @SuppressWarnings("nls") public class GerritClientSSH implements GerritClient { private final static Logger LOG = LoggerFactory.getLogger(GerritClientSSH.class); private final static int TIMEOUT = 2500; private static final int SLEEP_INTERVAL = 500; private static final char[] REPO_NAME_INVALID_CHARS = { '\\', ':', '~', '?', '*', '<', '>', '|', '%', '"' }; private static final int DEFAULT_SSH_PORT = 29418; enum Tables { ACCOUNTS, ACCOUNT_AGREEMENTS, ACCOUNT_DIFF_PREFERENCES, ACCOUNT_EXTERNAL_IDS, ACCOUNT_GROUPS, ACCOUNT_GROUP_AGREEMENTS, ACCOUNT_GROUP_MEMBERS, ACCOUNT_GROUP_MEMBERS_AUDIT, ACCOUNT_GROUP_NAMES, ACCOUNT_PATCH_REVIEWS, ACCOUNT_PROJECT_WATCHES, ACCOUNT_SSH_KEYS, APPROVAL_CATEGORIES, APPROVAL_CATEGORY_VALUES, CHANGES, CHANGE_MESSAGES, CONTRIBUTOR_AGREEMENTS, PATCH_COMMENTS, PATCH_SETS, PATCH_SET_ANCESTORS, PATCH_SET_APPROVALS, PROJECTS, REF_RIGHTS, SCHEMA_VERSION, STARRED_CHANGES, SYSTEM_CONFIG, TRACKING_IDS; @Override public String toString() { return name(); } } enum ResultFormat { PRETTY, JSON } enum Cache { ALL, PROJECTS, GROUPS } private static final String GERRIT_VERSION_PREFIX = "gerrit version "; private final String ACCOUNTS_PREFIX = "username:"; private final int ACCOUNTS_QUERY_BLOCKSIZE = 100; private final Pattern UNSUPPORTED_GSQL = Pattern.compile( ".*(show|insert|update|delete|merge|create|alter|rename|truncate|drop)\\s.*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); final GerritServerConfig gerritConfig; final int port; final String onBehalfOf; JSch client = null; Session session = null; ChannelExec channel = null; GerritVersion serverVersion = null; HashMap<String,GerritPlugin> plugins = null; public GerritClientSSH(GerritServerConfig gerritConfig, String onBehalfOf) { this.gerritConfig = gerritConfig; this.port = NumberUtils.toInt(gerritConfig.getPort(), DEFAULT_SSH_PORT); this.onBehalfOf = onBehalfOf; } @Override public void connect() throws ConnectionException { LOG.info(MessageFormat.format("Trying to connect to Gerrit {0}:{1}.", gerritConfig.getHost(), port)); File privateKeyFile = null; try { client = new JSch(); JSch.setLogger(new JschLogger()); Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); config.put("server_host_key", "ssh-rsa"); JSch.setConfig(config); privateKeyFile = getPrivateKeyFile(gerritConfig.getPrivateKey()); client.addIdentity(privateKeyFile.getAbsolutePath(), gerritConfig.getPassphrase()); session = client.getSession(gerritConfig.getUser(), gerritConfig.getHost(), port); session.setTimeout(TIMEOUT); session.connect(); } catch (JSchException e) { throw andDisconnect(new ConnectionException("Failed to connect to Gerrit", e)); } finally { if (privateKeyFile != null) { privateKeyFile.delete(); } } LOG.info(String.format("Connected to Gerrit %s:%s (%s)", gerritConfig.getHost(), port, session.getServerVersion())); } @Override public GerritVersion getVersion() throws ConnectionException, CommandException { if (serverVersion == null) { List<String> result = null; try { result = sshCommand("gerrit version"); } catch (CommandException e) { throw andDisconnect(new CommandException("Failed to retrieve Gerrit version", e)); } if (result.size() != 1) { throw andDisconnect(new CommandException(MessageFormat.format( "Failed to retrieve Gerrit version: Invalid result size ({0})", CollectionUtils.toString(result, ',')))); } String versionString = result.get(0); if (StringUtils.isBlank(versionString)) { return GerritVersion.GERRIT_UNKNOWN_VERSION; } if (!versionString.startsWith(GERRIT_VERSION_PREFIX)) { return GerritVersion.GERRIT_UNKNOWN_VERSION; } serverVersion = GerritVersion.asGerritVersion(versionString.substring(GERRIT_VERSION_PREFIX.length())); } return serverVersion; } @Override public Map<String,GerritPlugin> getPlugins() throws ConnectionException, CommandException { if (plugins == null) { plugins = new HashMap<String,GerritPlugin>(); if (getVersion().supports(GerritFeature.LS_PLUGINS)) { List<String> sshResponse = null; try { sshResponse = sshCommand("gerrit plugin ls --all"); if (sshResponse.size() > 2) { // skip the first two lines; they contain just a table header for (int i = 2; i < sshResponse.size(); ++i) { GerritPlugin plugin = GerritPlugin.valueOf(sshResponse.get(i)); if (plugin != null) { plugins.put(plugin.getName(), plugin); if (LOG.isInfoEnabled()) { LOG.info(MessageFormat.format("plugin: ''{0}''", plugin)); } } } } } catch (CommandException e) { // fail gracefully and assume that there are no plugins available LOG.error("Failed to retrieve Gerrit plugins", e); disconnect(); } } } return plugins; } private File getPrivateKeyFile(String privateKey) { File privateKeyFile = null; try { privateKeyFile = File.createTempFile("gerrit_key", "ssh"); FileUtils.writeStringToFile(privateKeyFile, privateKey, "ISO8859_1"); } catch (IOException e) { LOG.error("Failed to write key file."); //$NON-NLS-1$ throw new RuntimeException("Failed to write key file.", e); //$NON-NLS-1$ } return privateKeyFile; } @Override public void disconnect() { if (channel != null) { channel.disconnect(); channel = null; } if (session != null) { session.disconnect(); session = null; } LOG.info("Disconnected"); } @Override public void createProject(String name, String branch, Set<String> ownerList, String parent, boolean permissionsOnly, String description, SubmitType submitType, boolean useContributorAgreements, boolean useSignedOffBy, boolean emptyCommit) throws ConnectionException, CommandException { ProjectOptions options = new ProjectOptions(); options.setName(name); options.setBranch(branch); options.setOwners(ownerList); options.setParent(parent); options.setPermissionsOnly(permissionsOnly); options.setDescription(description); options.setSubmitType(submitType); options.setUseContributorAgreements(InheritableBoolean.valueOf(useContributorAgreements)); options.setUseSignedOffBy(InheritableBoolean.valueOf(useSignedOffBy)); options.setRequiredChangeId(InheritableBoolean.TRUE); options.setUseContentMerge(InheritableBoolean.TRUE); options.setCreateEmptyCommit(emptyCommit); createProject(options); } @Override public void createProject(ProjectOptions options) throws ConnectionException, CommandException { String name = options.getName(); if (name == null) { throw andDisconnect(new IllegalArgumentException("'name' is required")); } String checkFailedMsg = checkProjectName(name); if (checkFailedMsg != null) { throw andDisconnect(new IllegalArgumentException(checkFailedMsg)); } GerritVersion version = getVersion(); sshCommand(sshCreateProject(options, version)); } String sshCreateProject(ProjectOptions options, GerritVersion version) throws ConnectionException, CommandException { StringBuilder sb = new StringBuilder("gerrit create-project"); appendOption(sb, "name", options.getName()); appendOption(sb, "branch", options.getBranches()); appendOption(sb, "owner", options.getOwners()); appendOption(sb, "parent", options.getParent()); appendOption(sb, "permissions-only", options.isPermissionsOnly()); appendOption(sb, "description", options.getDescription()); appendOption(sb, "submit-type", options.getSubmitType() != null ? options.getSubmitType().name() : null); appendOption(sb, "use-contributor-agreements", options.getUseContributorAgreements()); appendOption(sb, "use-signed-off-by", options.getUseSignedOffBy()); appendOption(sb, "require-change-id", options.getRequiredChangeId()); appendOption(sb, "use-content-merge", options.getUseContentMerge()); appendOption(sb, "empty-commit", options.isCreateEmptyCommit()); if (version.supports(GerritFeature.CREATE_PROJECT_MAX_SIZE)) { appendOption(sb, "max-object-size-limit", options.getMaxObjectSizeLimit()); } if (version.supports(GerritFeature.CREATE_PROJECT_PLUGIN_CONFIG)) { for (String pluginName: options.getPluginConfigKeys()) { GerritPlugin plugin = getPlugins().get(pluginName); if (plugin != null && plugin.isEnabled() && gerritConfig.isEnabled(pluginName)) { for (Entry<String,String> pluginConfig: options.getPluginConfig(pluginName).entrySet()) { appendOption(sb, "plugin-config", pluginName + "." + pluginConfig.getKey() + "=" + pluginConfig.getValue()); } } } } return sb.toString(); } @Override public List<String> getProjects() throws ConnectionException, CommandException { return getProjects("all"); } @Override public List<String> getProjects(String type) throws ConnectionException, CommandException { GerritVersion version = getVersion(); StringBuilder sb = new StringBuilder("gerrit ls-projects"); if (version.supports(GerritFeature.LS_PROJECTS_TYPE_ATTR)) { appendOption(sb, "type", type); } return sshCommand(sb.toString()); } @Override public boolean projectExists(final String name) throws ConnectionException, CommandException { if (name == null) { return false; } return getProjects().contains(name); } @Override public void createGroup(final String name, final String owner, final String description, final Set<String> members) throws ConnectionException, CommandException { if (name == null) { throw andDisconnect(new IllegalArgumentException("'name' is required")); } String checkFailedMsg = checkGroupName(name); if (checkFailedMsg != null) { throw andDisconnect(new IllegalArgumentException(checkFailedMsg)); } StringBuilder sb = new StringBuilder("gerrit create-group"); appendOption(sb, "owner", owner); appendOption(sb, "description", description); appendOption(sb, "member", getKnownAccounts(members)); appendOption(sb, "visible-to-all", true); appendArgument(sb, name); sshCommand(sb.toString()); } @Override public List<String> getGroups() throws ConnectionException, CommandException { List<String> result = Collections.emptyList(); GerritVersion version = getVersion(); if (version.supports(GerritFeature.LS_GROUPS)) { StringBuilder sb = new StringBuilder("gerrit ls-groups"); if (version.supports(GerritFeature.LS_GROUPS_VISIBLE_TO_ALL_ATTR)) { appendOption(sb, "visible-to-all", true); } result = sshCommand(sb.toString()); } else { result = new ArrayList<String>(); List<String> gsqlResult = gsql("SELECT name FROM " + Tables.ACCOUNT_GROUPS, ResultFormat.JSON); for (final String entry : gsqlResult) { if (isRow(entry)) { result.add(JSONUtils.getString(entry, "columns.name")); } } } return result; } @Override public List<String> getGroups(String... projectNames) throws ConnectionException, CommandException { List<String> result = Collections.emptyList(); if (projectNames == null || projectNames.length == 0) { return result; } // Gerrit throws exceptions for --project options that correspond to // no Gerrit project; thus, we have to filter out thise project names before // sending the ls-groups command Set<String> allProjects = new HashSet<String>(getProjects()); GerritVersion version = getVersion(); if (version.supports(GerritFeature.LS_GROUPS_PROJECT_ATTR)) { StringBuilder sb = new StringBuilder("gerrit ls-groups"); if (version.supports(GerritFeature.LS_GROUPS_VISIBLE_TO_ALL_ATTR)) { appendOption(sb, "visible-to-all", true); } for (String projectName : projectNames) { if (allProjects.contains(projectName)) { appendOption(sb, "project", projectName); } } result = sshCommand(sb.toString()); } else if (version.supports(GerritFeature.REF_RIGHTS_TABLE)) { result = new ArrayList<String>(); StringBuilder sb = new StringBuilder(); sb.append("SELECT name FROM ").append(Tables.ACCOUNT_GROUP_NAMES) .append(" WHERE group_id IN (SELECT group_id FROM ").append(Tables.REF_RIGHTS).append(" WHERE"); for (String projectName : projectNames) { if (allProjects.contains(projectName)) { sb.append(" project_name='").append(projectName).append("' OR"); } } sb.replace(sb.length() - 3, sb.length(), ""); sb.append(");"); List<String> gsqlResult = gsql(sb.toString(), ResultFormat.JSON); for (String entry : gsqlResult) { if (isRow(entry)) { result.add(JSONUtils.getString(entry, "columns.name")); } } } return result; } @Override public boolean groupExists(final String name) throws ConnectionException, CommandException { if (name == null) { return false; } List<String> groups = getGroups(); for (String group: groups) { if (name.equals(group)) { return true; } } return false; } @Override public Set<String> getKnownAccounts(Set<String> variousAccounts) throws ConnectionException, CommandException { if (variousAccounts == null || variousAccounts.isEmpty()) { return Collections.emptySet(); } Set<String> result = new HashSet<String>(); GerritVersion version = getVersion(); if (version.supports(GerritFeature.ACCOUNT_CHECK_OBSOLETE)) { for (String account: variousAccounts) { if (StringUtils.isNotBlank(account)) { result.add(account); } } } else { int variousAccountsSize = variousAccounts.size(); int blocks = (int) Math.ceil((float) variousAccountsSize / ACCOUNTS_QUERY_BLOCKSIZE); for (int i = 0; i < blocks; i++) { List<String> worklist = new ArrayList<String>(variousAccounts); int startIndex = i * ACCOUNTS_QUERY_BLOCKSIZE; int endIndex = Math.min((i + 1) * ACCOUNTS_QUERY_BLOCKSIZE, variousAccountsSize); result.addAll(queryKnownAccounts(worklist.subList(startIndex, endIndex))); } } return result; } @Override public String checkGroupName(String name) { if (StringUtils.isBlank(name)) { return "Group names must not be blank"; } if (StringUtils.trim(name).length() < name.length()) { return "Group names must not start or end with whitespace"; } if (containsWhitespace(name, true)) { return "Group names must not contain whitespace"; } if (HtmlUtils.containsTags(name)) { return "Group names must not contain HTML tags"; } return null; } @Override public String checkProjectName(String name) { if (StringUtils.isBlank(name)) { return "Repository names must not be blank"; } if (StringUtils.trim(name).length() < name.length()) { return "Repository names must not start or end with whitespace"; } if (containsWhitespace(name, false)) { return "Repository names must not contain whitespace"; } if (name.startsWith("/")) { return "Repository names must not start with a slash"; } if (name.endsWith("/")) { return "Repository names must not end with a trailing slash"; } if (HtmlUtils.containsTags(name)) { return "Repository names must not contain HTML tags"; } if (StringUtils.containsAny(name, REPO_NAME_INVALID_CHARS )) { return "Repository names must not contain any of the following characters: " + "'\', ':', '~', '?', '*', '<', '>', '|', '%', '\"'"; } if (name.startsWith("../") //$NON-NLS-1$ || name.contains("/../") //$NON-NLS-1$ || name.contains("/./")) { //$NON-NLS-1$ return "Repository names must not contain \"../\", \"/../\" or \"/./\""; } return null; } private boolean containsWhitespace(String s, boolean allowBlanks) { for (int i = 0; i < s.length() ; i++) { char c = s.charAt(i); if (allowBlanks && c == ' ') { continue; } if (Character.isWhitespace(c)) { return true; } if (c == '\0') { return true; } } return false; } /** * Utility method for checking accounts. * * This indirection was introduced to allow splitting the call if the parameter list is huge. * Depending on the database this could easily fail. Hence split it into separate SQL queries * and merge the results. * * @throws ConnectionException in case of connection / communication problems * @throws CommandException in case of unsuccessful commands */ private Collection<String> queryKnownAccounts(Collection<String> variousAccounts) throws ConnectionException, CommandException { final List<String> result = new ArrayList<String>(); StringBuilder sb = new StringBuilder(); sb.append("SELECT external_id FROM ").append(Tables.ACCOUNT_EXTERNAL_IDS) .append(" WHERE external_id IN ("); boolean noRealParameters = true; for (String variousAccount : variousAccounts) { if (!StringUtils.isBlank(variousAccount)) { sb.append("'").append(ACCOUNTS_PREFIX).append(variousAccount).append("', "); noRealParameters = false; } } sb.delete(sb.length() - 2, sb.length()); sb.append(");"); if (noRealParameters) { return result; } final List<String> gsqlResult = gsql(sb.toString(), ResultFormat.JSON); for (final String entry : gsqlResult) { if (isRow(entry)) { result.add(StringUtils.removeStart(JSONUtils.getString(entry, "columns.external_id"), ACCOUNTS_PREFIX)); } } return result; } /** * Performs a single GSQL statement according to <a href= * "http://gerrit.googlecode.com/svn/documentation/2.1.5/cmd-gsql.html" * >gerrit gsql</a> (<a href= * "http://gerrit.googlecode.com/svn/documentation/2.1.5/cmd-gsql.html#options" * >options</a>). * * Note that only SELECT statements are allowed * * @param query * the query to execute (only SELECT allowed) * @param format * <code>PRETTY</code> or <code>JSON</code> * * @return the resulting lines depending in the specified format * <code>format</code>. The last line includes query statistics. * * @throws ConnectionException in case of connection / communication problems * @throws CommandException in case of unsuccessful commands */ @Deprecated List<String> gsql(final String query, final ResultFormat format) throws ConnectionException, CommandException { if (StringUtils.isBlank(query)) { LOG.info("No query passed. Returning an empty result."); return Collections.emptyList(); } // only allow READ access via gsql() if (UNSUPPORTED_GSQL.matcher(query).matches()) { throw new UnsupportedOperationException( String.format("Your command contains unsupported GSQL: '%s'", query)); } StringBuilder sb = new StringBuilder("gerrit gsql"); sb.append(" --format ").append(format.name()); sb.append(" -c \"").append(query).append("\""); return sshCommand(sb.toString()); } /** * Performs a SSH command * * @param command * the command to execute * * @return the resulting lines * * @throws ConnectionException in case of connection / communication problems * @throws CommandException in case of unsuccessful commands */ private List<String> sshCommand(final String command) throws ConnectionException, CommandException { LOG.info(MessageFormat.format("Sending on behalf of ''{0}'': ''{1}''", onBehalfOf, command)); boolean manuallyConnected = false; ByteArrayOutputStream baosOut = new ByteArrayOutputStream(); ByteArrayOutputStream baosErr = new ByteArrayOutputStream(); ByteArrayInputStream baisIn = new ByteArrayInputStream(new byte[0]); ChannelExec channel = null; try { if (client == null || session == null) { connect(); manuallyConnected = true; } channel = (ChannelExec) session.openChannel("exec"); channel.setInputStream(baisIn); channel.setOutputStream(baosOut); channel.setErrStream(baosErr); channel.setCommand(command); channel.connect(); while (!channel.isClosed()) { try { Thread.sleep(SLEEP_INTERVAL); } catch (InterruptedException e) { throw andDisconnect(new CommandException()); } } List<String> result = new LinkedList<String>(); InputStreamReader inR = new InputStreamReader(new ByteArrayInputStream(baosOut.toByteArray()), "ISO-8859-1"); BufferedReader buf = new BufferedReader(inR); String line; while ((line = buf.readLine()) != null) { result.add(line); } if (result.size() > 0) { checkForErrorsInResponse(result.get(0)); } if (baosErr.size() > 0) { InputStreamReader errISR = new InputStreamReader(new ByteArrayInputStream(baosErr.toByteArray()), "ISO-8859-1"); BufferedReader errBR = new BufferedReader(errISR); StringBuilder errSB = new StringBuilder("Gerrit CLI returned with an error:"); String errLine; while ((errLine = errBR.readLine()) != null) { errSB.append("\n").append(errLine); } throw andDisconnect(new CommandException(errSB.toString())); } return result; } catch (JSchException e) { throw andDisconnect(new ConnectionException("Failed to create/open channel.", e)); } catch (IOException e) { throw andDisconnect(new ConnectionException("Failed to read errors from channel.", e)); } finally { closeQuietly(channel, baisIn, baosOut, baosErr, manuallyConnected); } } private void closeQuietly(ChannelExec channel, ByteArrayInputStream baisIn, ByteArrayOutputStream baosOut, ByteArrayOutputStream baosErr, boolean forceDisconnect) { if (channel != null) { IOUtils.closeQuietly(baisIn); IOUtils.closeQuietly(baosOut); IOUtils.closeQuietly(baosErr); channel.disconnect(); if (forceDisconnect) { disconnect(); } } } /** * Unfortunately Gerrit sometimes returns its error messages in the normal response instead of the error stream. * Therefore this utility method should check for common erros and could be extended accordingly. * * @param firstLine * * @throws CommandException in case of unsuccessful commands */ private void checkForErrorsInResponse(String firstLine) throws CommandException { if (firstLine == null) { return; } if (firstLine.startsWith("Error when trying to")) { throw andDisconnect(new CommandException(firstLine)); } if (firstLine.startsWith("{\"type\":\"error\"")) { throw andDisconnect(new CommandException(String.format("Command returned with error: '%s'", JSONUtils.getString(firstLine, "message")))); } } /** * Appends an argument to the string buffer, if the given <code>value</code> * is not null or blank. The value is enclosed in double quotes. * * @param sb the buffer that is worked on. * @param value the argument value. */ private void appendArgument(StringBuilder sb, String value) { if (!StringUtils.isBlank(value)) { sb.append(" \"").append(value).append("\""); } } /** * Appends a boolean option to the string buffer, e.g. <tt>"--permissionsOnly"</tt>. * Note that the value is not rendered. If the given <code>value</code> evaluated * to <code>false</code> the option is not rendered at all. * * @param sb the buffer that is worked on. * @param name the name of the option. * @param value the value of the option. */ private void appendOption(StringBuilder sb, String name, boolean value) { if (StringUtils.isNotBlank(name) && value == true) { sb.append(" --").append(name); } } /** * Appends a boolean option with the possible values <code>TRUE</code>, <code>FALSE</code> * or <code>INHERITED</code> to the string buffer. * Note that the value is not rendered. If the given <code>value</code> does not evaluate * to {@link InheritableBoolean#TRUE} the option is not rendered at all. * * @param sb the buffer that is worked on. * @param name the name of the option. * @param value the value of the option. */ private void appendOption(StringBuilder sb, String name, InheritableBoolean value) { if (StringUtils.isNotBlank(name) && InheritableBoolean.TRUE == value) { sb.append(" --").append(name); } } /** * Appends a string option to the string buffer, if the given <code>value</code> * is not null or blank. The value is enclosed in double quotes. * * @param sb the buffer that is worked on. * @param name the name of the option. * @param value the value of the option. */ private void appendOption(StringBuilder sb, String name, String value) { if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(value)) { sb.append(" --").append(name).append(" \"").append(value).append("\""); } } /** * Appends a multi-valued option to the string buffer, if the given <code>values</code> * is not null or empty. This method renders multiple options of the form <tt>--name=value</tt>. * * @param sb the buffer that is worked on. * @param name the name of the option. * @param value the collection of values. */ private void appendOption(StringBuilder sb, String name, Collection<String> values) { if (CollectionUtils.isNotBlank(values)) { for (String value : values) { appendOption(sb, name, value); } } } /** * Checks whether a returned (JSON) string is a GSQL table row * * @param entry * the entry as serialized JSON String * * @return <code>true</code> if it starts with * <code>{"type":"row";</code>, otherwise * <code>false</code> */ boolean isRow(final String entry) { return entry.startsWith("{\"type\":\"row\""); } /** * Terminate connection in error case, before throwing the exception <code>e</code> * * @throws T */ private <T extends Throwable> T andDisconnect(T e) { LOG.error("The last command could not be completed", e); disconnect(); return e; } }