/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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 org.apache.geode.management.internal.web.shell;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.geode.internal.lang.Filter;
import org.apache.geode.internal.lang.Initable;
import org.apache.geode.internal.lang.StringUtils;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.util.CollectionUtils;
import org.apache.geode.management.internal.cli.CommandRequest;
import org.apache.geode.management.internal.cli.i18n.CliStrings;
import org.apache.geode.management.internal.cli.shell.Gfsh;
import org.apache.geode.management.internal.web.domain.Link;
import org.apache.geode.management.internal.web.domain.LinkIndex;
import org.apache.geode.management.internal.web.http.ClientHttpRequest;
import org.apache.geode.management.internal.web.http.HttpHeader;
import org.apache.geode.management.internal.web.util.ConvertUtils;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.util.UriTemplate;
/**
* The RestHttpOperationInvoker class is an implementation of the OperationInvoker interface that
* translates (adapts) GemFire shell command invocations into HTTP requests to a corresponding REST
* API call hosted by the GemFire Manager's HTTP service using the Spring RestTemplate.
*
* @see org.apache.geode.internal.lang.Initable
* @see org.apache.geode.management.internal.cli.shell.Gfsh
* @see org.apache.geode.management.internal.cli.shell.OperationInvoker
* @see org.apache.geode.management.internal.web.shell.AbstractHttpOperationInvoker
* @see org.apache.geode.management.internal.web.shell.HttpOperationInvoker
* @see org.apache.geode.management.internal.web.shell.SimpleHttpOperationInvoker
* @since GemFire 8.0
*/
@SuppressWarnings("unused")
public class RestHttpOperationInvoker extends AbstractHttpOperationInvoker implements Initable {
private static final Logger logger = LogService.getLogger();
protected static final String ENVIRONMENT_VARIABLE_REQUEST_PARAMETER_PREFIX = "vf.gf.env.";
protected static final String RESOURCES_REQUEST_PARAMETER = "resources";
// the HttpOperationInvoker used when this RestHttpOperationInvoker is unable to resolve the
// correct REST API
// web service endpoint (URI) for a command
private final HttpOperationInvoker httpOperationInvoker;
// the LinkIndex containing Links to all GemFire REST API web service endpoints
private final LinkIndex linkIndex;
/**
* Constructs an instance of the RestHttpOperationInvoker class initialized with the given link
* index containing links referencing all REST API web service endpoints. This constructor should
* only be used for testing purposes.
*
* @param linkIndex the LinkIndex containing Links to all REST API web service endpoints in
* GemFire's REST interface.
* @see org.apache.geode.management.internal.web.domain.LinkIndex
*/
RestHttpOperationInvoker(final LinkIndex linkIndex) {
super(REST_API_URL);
assertNotNull(linkIndex,
"The Link Index resolving commands to REST API web service endpoints cannot be null!");
this.linkIndex = linkIndex;
this.httpOperationInvoker = new SimpleHttpOperationInvoker();
}
/**
* Constructs an instance of the RestHttpOperationInvoker class initialized with the given link
* index containing links referencing all REST API web service endpoints. In addition, a reference
* to the instance of GemFire shell (Gfsh) using this RestHttpOperationInvoker to send command
* invocations to the GemFire Manager's HTTP service via HTTP for processing is required in order
* to interact with the shell and provide feedback to the user.
*
* @param linkIndex the LinkIndex containing Links to all REST API web service endpoints in
* GemFire' REST interface.
* @param gfsh a reference to the instance of the GemFire shell using this OperationInvoker to
* process commands.
* @see #RestHttpOperationInvoker(org.apache.geode.management.internal.web.domain.LinkIndex,
* org.apache.geode.management.internal.cli.shell.Gfsh, Map)
* @see org.apache.geode.management.internal.cli.shell.Gfsh
* @see org.apache.geode.management.internal.web.domain.LinkIndex
*/
public RestHttpOperationInvoker(final LinkIndex linkIndex, final Gfsh gfsh,
Map<String, String> securityProperties) {
this(linkIndex, gfsh, CliStrings.CONNECT__DEFAULT_BASE_URL, securityProperties);
}
/**
* Constructs an instance of the RestHttpOperationInvoker class initialized with the given link
* index containing links referencing all REST API web service endpoints. In addition, a reference
* to the instance of GemFire shell (Gfsh) using this RestHttpOperationInvoker to send command
* invocations to the GemFire Manager's HTTP service via HTTP for processing is required in order
* to interact with the shell and provide feedback to the user. Finally, a URL to the HTTP service
* running in the GemFire Manager is specified as the base location for all HTTP requests.
*
* @param linkIndex the LinkIndex containing Links to all REST API web service endpoints in
* GemFire's REST interface.
* @param gfsh a reference to the instance of the GemFire shell using this OperationInvoker to
* process commands.
* @param baseUrl the String specifying the base URL to the GemFire Manager's HTTP service, REST
* interface.
* @see org.apache.geode.management.internal.web.domain.LinkIndex
* @see org.apache.geode.management.internal.cli.shell.Gfsh
*/
public RestHttpOperationInvoker(final LinkIndex linkIndex, final Gfsh gfsh, final String baseUrl,
Map<String, String> securityProperties) {
super(gfsh, baseUrl, securityProperties);
assertNotNull(linkIndex,
"The Link Index resolving commands to REST API web service endpoints cannot be null!");
this.linkIndex = linkIndex;
this.httpOperationInvoker = new SimpleHttpOperationInvoker(gfsh, baseUrl, securityProperties);
}
/**
* Initializes the RestHttpOperationInvokers scheduled and periodic monitoring task to assess the
* availibity of the targeted GemFire Manager's HTTP service.
*
* @see org.apache.geode.internal.lang.Initable#init()
* @see org.springframework.http.client.ClientHttpRequest
*/
@SuppressWarnings("null")
public void init() {
final Link pingLink = getLinkIndex().find(PING_LINK_RELATION);
if (pingLink != null) {
if (logger.isDebugEnabled()) {
logger.debug(
"Scheduling periodic HTTP ping requests to monitor the availability of the GemFire Manager HTTP service @ ({})",
getBaseUrl());
}
getExecutorService().scheduleAtFixedRate(new Runnable() {
public void run() {
try {
org.springframework.http.client.ClientHttpRequest httpRequest = getRestTemplate()
.getRequestFactory().createRequest(pingLink.getHref(), HttpMethod.HEAD);
httpRequest.getHeaders().set(HttpHeader.USER_AGENT.getName(),
USER_AGENT_HTTP_REQUEST_HEADER_VALUE);
httpRequest.getHeaders().setAccept(getAcceptableMediaTypes());
httpRequest.getHeaders().setContentLength(0l);
if (securityProperties != null) {
Iterator<Entry<String, String>> it = securityProperties.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> entry = it.next();
httpRequest.getHeaders().add(entry.getKey(), entry.getValue());
}
}
ClientHttpResponse httpResponse = httpRequest.execute();
if (HttpStatus.NOT_FOUND.equals(httpResponse.getStatusCode())) {
throw new IOException(String.format(
"The HTTP service at URL (%1$s) could not be found!", pingLink.getHref()));
} else if (!HttpStatus.OK.equals(httpResponse.getStatusCode())) {
printDebug(
"Received unexpected HTTP status code (%1$d - %2$s) for HTTP request (%3$s).",
httpResponse.getRawStatusCode(), httpResponse.getStatusText(),
pingLink.getHref());
}
} catch (IOException e) {
printDebug("An error occurred while connecting to the Manager's HTTP service: %1$s: ",
e.getMessage());
getGfsh().notifyDisconnect(RestHttpOperationInvoker.this.toString());
stop();
}
}
}, DEFAULT_INITIAL_DELAY, DEFAULT_PERIOD, DEFAULT_TIME_UNIT);
} else {
if (logger.isDebugEnabled()) {
logger.debug(
"The Link to the GemFire Manager web service endpoint @ ({}) to monitor availability was not found!",
getBaseUrl());
}
}
initClusterId();
}
/**
* Returns a reference to an implementation of HttpOperationInvoker used as the fallback by this
* RestHttpOperationInvoker for processing commands via HTTP requests.
*
* @return an instance of HttpOperationInvoker used by this RestHttpOperationInvoker as a fallback
* to process commands via HTTP requests.
* @see org.apache.geode.management.internal.web.shell.HttpOperationInvoker
*/
protected HttpOperationInvoker getHttpOperationInvoker() {
return httpOperationInvoker;
}
/**
* Returns the LinkIndex resolving Gfsh commands to GemFire REST API web service endpoints. The
* corresponding web service endpoint is a URI/URL uniquely identifying the resource on which the
* command was invoked.
*
* @return the LinkIndex containing Links for all GemFire REST API web service endpoints.
* @see org.apache.geode.management.internal.web.domain.LinkIndex
*/
protected LinkIndex getLinkIndex() {
return linkIndex;
}
/**
* Creates an HTTP request from the specified command invocation encapsulated by the
* CommandRequest object. The CommandRequest identifies the resource targeted by the command
* invocation along with any parameters to be sent as part of the HTTP request.
*
* @param command the CommandRequest object encapsulating details of the command invocation.
* @return a client HTTP request detailing the operation to be performed on the remote resource
* targeted by the command invocation.
* @see AbstractHttpOperationInvoker#createHttpRequest(org.apache.geode.management.internal.web.domain.Link)
* @see org.apache.geode.management.internal.cli.CommandRequest
* @see org.apache.geode.management.internal.web.http.ClientHttpRequest
* @see org.apache.geode.management.internal.web.util.ConvertUtils#convert(byte[][])
*/
protected ClientHttpRequest createHttpRequest(final CommandRequest command) {
ClientHttpRequest request = createHttpRequest(findLink(command));
Map<String, String> commandParameters = command.getParameters();
for (Map.Entry<String, String> entry : commandParameters.entrySet()) {
if (NullValueFilter.INSTANCE.accept(entry)) {
request.addParameterValues(entry.getKey(), entry.getValue());
}
}
Map<String, String> environmentVariables = command.getEnvironment();
for (Map.Entry<String, String> entry : environmentVariables.entrySet()) {
if (EnvironmentVariableFilter.INSTANCE.accept(entry)) {
request.addParameterValues(ENVIRONMENT_VARIABLE_REQUEST_PARAMETER_PREFIX + entry.getKey(),
entry.getValue());
}
}
if (command.getFileData() != null) {
request.addParameterValues(RESOURCES_REQUEST_PARAMETER,
(Object[]) ConvertUtils.convert(command.getFileData()));
}
return request;
}
/**
* Finds a Link from the Link Index containing the HTTP request URI to the web service endpoint
* for the relative operation on the resource.
*
* @param relation a String describing the relative operation (state transition) on the resource.
* @return an instance of Link containing the HTTP request URI used to perform the intended
* operation on the resource.
* @see #getLinkIndex()
* @see org.apache.geode.management.internal.web.domain.Link
* @see org.apache.geode.management.internal.web.domain.LinkIndex#find(String)
*/
@Override
protected Link findLink(final String relation) {
return getLinkIndex().find(relation);
}
/**
* Finds a Link from the Link Index corresponding to the command invocation. The CommandRequest
* indicates the intended function on the target resource so the proper Link based on it's
* relation (the state transition of the corresponding function), along with it's method of
* operation and corresponding REST API web service endpoint (URI), can be identified.
*
* @param command the CommandRequest object encapsulating the details of the command invocation.
* @return a Link referencing the correct REST API web service endpoint (URI) and method for the
* command invocation.
* @see #getLinkIndex()
* @see #resolveLink(org.apache.geode.management.internal.cli.CommandRequest, java.util.List)
* @see org.apache.geode.management.internal.cli.CommandRequest
* @see org.apache.geode.management.internal.web.domain.Link
* @see org.apache.geode.management.internal.web.domain.LinkIndex
*/
protected Link findLink(final CommandRequest command) {
List<Link> linksFound = new ArrayList<>(getLinkIndex().size());
for (Link link : getLinkIndex()) {
if (command.getInput().startsWith(link.getRelation())) {
linksFound.add(link);
}
}
if (linksFound.isEmpty()) {
throw new RestApiCallForCommandNotFoundException(
String.format("No REST API call for command (%1$s) was found!", command.getInput()));
}
return (linksFound.size() > 1 ? resolveLink(command, linksFound) : linksFound.get(0));
}
/**
* Resolves one Link from a Collection of Links based on the command invocation matching multiple
* relations from the Link Index.
*
* @param command the CommandRequest object encapsulating details of the command invocation.
* @param links a Collection of Links for the command matching the relation.
* @return the resolved Link matching the command exactly as entered by the user.
* @see #findLink(org.apache.geode.management.internal.cli.CommandRequest)
* @see org.apache.geode.management.internal.cli.CommandRequest
* @see org.apache.geode.management.internal.web.domain.Link
* @see org.springframework.web.util.UriTemplate
*/
// Find and use the Link with the greatest number of path variables that can be expanded!
protected Link resolveLink(final CommandRequest command, final List<Link> links) {
// NOTE, Gfsh's ParseResult contains a Map entry for all command options whether or not the user
// set the option
// with a value on the command-line, argh!
Map<String, String> commandParametersCopy =
CollectionUtils.removeKeys(new HashMap<>(command.getParameters()), NoValueFilter.INSTANCE);
Link resolvedLink = null;
int pathVariableCount = 0;
for (Link link : links) {
final List<String> pathVariables =
new UriTemplate(decode(link.getHref().toString())).getVariableNames();
// first, all path variables in the URL/URI template must be resolvable/expandable for this
// Link
// to even be considered...
if (commandParametersCopy.keySet().containsAll(pathVariables)) {
// then, either we have not found a Link for the command yet, or the number of
// resolvable/expandable
// path variables in this Link has to be greater than the number of resolvable/expandable
// path variables
// for the last Link
if (resolvedLink == null || (pathVariables.size() > pathVariableCount)) {
resolvedLink = link;
pathVariableCount = pathVariables.size();
}
}
}
if (resolvedLink == null) {
throw new RestApiCallForCommandNotFoundException(
String.format("No REST API call for command (%1$s) was found!", command.getInput()));
}
return resolvedLink;
}
/**
* Processes the requested command. Sends the command to the GemFire Manager for remote processing
* (execution).
*
* @param command the command requested/entered by the user to be processed.
* @return the result of the command execution.
* @see #createHttpRequest(org.apache.geode.management.internal.cli.CommandRequest)
* @see #handleResourceAccessException(org.springframework.web.client.ResourceAccessException)
* @see #isConnected()
* @see #send(org.apache.geode.management.internal.web.http.ClientHttpRequest, Class,
* java.util.Map)
* @see #simpleProcessCommand(org.apache.geode.management.internal.cli.CommandRequest,
* RestApiCallForCommandNotFoundException)
* @see org.apache.geode.management.internal.cli.CommandRequest
* @see org.springframework.http.ResponseEntity
*/
@Override
public String processCommand(final CommandRequest command) {
assertState(isConnected(),
"Gfsh must be connected to the GemFire Manager in order to process commands remotely!");
try {
ResponseEntity<String> response =
send(createHttpRequest(command), String.class, command.getParameters());
return response.getBody();
} catch (RestApiCallForCommandNotFoundException e) {
return simpleProcessCommand(command, e);
} catch (ResourceAccessException e) {
return handleResourceAccessException(e);
}
}
/**
* A method to process the command by sending an HTTP request to the simple URL/URI web service
* endpoint, where all details of the request and command invocation are encoded in the URL/URI.
*
* @param command the CommandRequest encapsulating the details of the command invocation.
* @param e the RestApiCallForCommandNotFoundException indicating the standard REST API web
* service endpoint could not be found.
* @return the result of the command execution.
* @see #getHttpOperationInvoker()
* @see org.apache.geode.management.internal.web.shell.HttpOperationInvoker#processCommand(org.apache.geode.management.internal.cli.CommandRequest)
* @see org.apache.geode.management.internal.cli.CommandRequest
*/
protected String simpleProcessCommand(final CommandRequest command,
final RestApiCallForCommandNotFoundException e) {
if (getHttpOperationInvoker() != null) {
printWarning(
"WARNING - No REST API web service endpoint (URI) exists for command (%1$s); using the non-RESTful, simple URI.",
command.getName());
return String.valueOf(getHttpOperationInvoker().processCommand(command));
}
throw e;
}
protected static class EnvironmentVariableFilter extends NoValueFilter {
protected static final EnvironmentVariableFilter INSTANCE = new EnvironmentVariableFilter();
@Override
public boolean accept(final Map.Entry<String, String> entry) {
return (!entry.getKey().startsWith("SYS") && super.accept(entry));
}
}
protected static class NoValueFilter implements Filter<Map.Entry<String, String>> {
protected static final NoValueFilter INSTANCE = new NoValueFilter();
@Override
public boolean accept(final Map.Entry<String, String> entry) {
return !StringUtils.isBlank(entry.getValue());
}
}
protected static class NullValueFilter implements Filter<Map.Entry<String, ?>> {
protected static final NullValueFilter INSTANCE = new NullValueFilter();
@Override
public boolean accept(final Map.Entry<String, ?> entry) {
return (entry.getValue() != null);
}
}
}