All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.geode.management.internal.web.shell.RestHttpOperationInvoker Maven / Gradle / Ivy

Go to download

Apache Geode provides a database-like consistency model, reliable transaction processing and a shared-nothing architecture to maintain very low latency performance with high concurrency processing

There is a newer version: 1.15.1
Show newest version
/*
 * 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 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 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> it = securityProperties.entrySet().iterator();
              while (it.hasNext()) {
                Entry 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 commandParameters = command.getParameters();

    for (Map.Entry entry : commandParameters.entrySet()) {
      if (NullValueFilter.INSTANCE.accept(entry)) {
        request.addParameterValues(entry.getKey(), entry.getValue());
      }
    }

    Map environmentVariables = command.getEnvironment();

    for (Map.Entry 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 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 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 commandParametersCopy =
        CollectionUtils.removeKeys(new HashMap<>(command.getParameters()), NoValueFilter.INSTANCE);

    Link resolvedLink = null;

    int pathVariableCount = 0;

    for (Link link : links) {
      final List 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 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 entry) {
      return (!entry.getKey().startsWith("SYS") && super.accept(entry));
    }
  }

  protected static class NoValueFilter implements Filter> {

    protected static final NoValueFilter INSTANCE = new NoValueFilter();

    @Override
    public boolean accept(final Map.Entry entry) {
      return !StringUtils.isBlank(entry.getValue());
    }
  }

  protected static class NullValueFilter implements Filter> {

    protected static final NullValueFilter INSTANCE = new NullValueFilter();

    @Override
    public boolean accept(final Map.Entry entry) {
      return (entry.getValue() != null);
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy