/*
* 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.controllers;
import java.io.IOException;
import java.util.Set;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.apache.geode.internal.GemFireVersion;
import org.apache.geode.internal.lang.ObjectUtils;
import org.apache.geode.internal.lang.StringUtils;
import org.apache.geode.internal.util.IOUtils;
import org.apache.geode.management.internal.cli.i18n.CliStrings;
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.domain.QueryParameterSource;
import org.apache.geode.management.internal.web.http.HttpMethod;
/**
* The ShellCommandsController class implements GemFire REST API calls for Gfsh Shell Commands.
*
* @see org.apache.geode.management.internal.cli.commands.ShellCommands
* @see org.apache.geode.management.internal.web.controllers.AbstractCommandsController
* @see org.springframework.stereotype.Controller
* @see org.springframework.web.bind.annotation.RequestBody
* @see org.springframework.web.bind.annotation.RequestMapping
* @see org.springframework.web.bind.annotation.RequestMethod
* @see org.springframework.web.bind.annotation.RequestParam
* @see org.springframework.web.bind.annotation.ResponseBody
* @since GemFire 8.0
*/
@Controller("shellController")
@RequestMapping(AbstractCommandsController.REST_API_VERSION)
@SuppressWarnings("unused")
public class ShellCommandsController extends AbstractCommandsController {
protected static final String MBEAN_ATTRIBUTE_LINK_RELATION = "mbean-attribute";
protected static final String MBEAN_OPERATION_LINK_RELATION = "mbean-operation";
protected static final String MBEAN_QUERY_LINK_RELATION = "mbean-query";
protected static final String PING_LINK_RELATION = "ping";
@RequestMapping(method = RequestMethod.POST, value = "/management/commands", params = "cmd")
@ResponseBody
public String command(@RequestParam("cmd") final String command) {
return processCommand(decode(command));
}
// TODO research the use of Jolokia instead
@RequestMapping(method = RequestMethod.GET, value = "/mbean/attribute")
public ResponseEntity<?> getAttribute(@RequestParam("resourceName") final String resourceName,
@RequestParam("attributeName") final String attributeName) {
try {
final Object attributeValue = getMBeanServer()
.getAttribute(ObjectName.getInstance(decode(resourceName)), decode(attributeName));
return new ResponseEntity<byte[]>(IOUtils.serializeObject(attributeValue), HttpStatus.OK);
} catch (AttributeNotFoundException e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.BAD_REQUEST);
} catch (InstanceNotFoundException e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.NOT_FOUND);
} catch (MalformedObjectNameException e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// TODO research the use of Jolokia instead
@RequestMapping(method = RequestMethod.POST, value = "/mbean/operation")
public ResponseEntity<?> invoke(@RequestParam("resourceName") final String resourceName,
@RequestParam("operationName") final String operationName,
@RequestParam(value = "signature", required = false) String[] signature,
@RequestParam(value = "parameters", required = false) Object[] parameters) {
signature = (signature != null ? signature : StringUtils.EMPTY_STRING_ARRAY);
parameters = (parameters != null ? parameters : ObjectUtils.EMPTY_OBJECT_ARRAY);
try {
final Object result = getMBeanServer().invoke(ObjectName.getInstance(decode(resourceName)),
decode(operationName), parameters, signature);
return new ResponseEntity<byte[]>(IOUtils.serializeObject(result), HttpStatus.OK);
} catch (InstanceNotFoundException e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.NOT_FOUND);
} catch (MalformedObjectNameException e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@RequestMapping(method = RequestMethod.POST, value = "/mbean/query")
public ResponseEntity<?> queryNames(@RequestBody final QueryParameterSource query) {
try {
final Set<ObjectName> objectNames =
getMBeanServer().queryNames(query.getObjectName(), query.getQueryExpression());
return new ResponseEntity<byte[]>(IOUtils.serializeObject(objectNames), HttpStatus.OK);
} catch (IOException e) {
return new ResponseEntity<String>(printStackTrace(e), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Gets a link index for the web service endpoints and REST API calls in GemFire for management
* and monitoring using GemFire shell (Gfsh).
*
* @return a LinkIndex containing Links for all web service endpoints, REST API calls in GemFire.
* @see org.apache.geode.management.internal.cli.i18n.CliStrings
* @see AbstractCommandsController#toUri(String, String)
* @see org.apache.geode.management.internal.web.domain.Link
* @see org.apache.geode.management.internal.web.domain.LinkIndex
* @see org.apache.geode.management.internal.web.http.HttpMethod
*/
// TODO figure out a better way to maintain this link index, such as using an automated way to
// introspect
// the Spring Web MVC Controller RequestMapping Annotations.
@RequestMapping(method = RequestMethod.GET, value = "/index",
produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public LinkIndex index(@RequestParam(value = "scheme", required = false,
defaultValue = "http") final String scheme) {
// logger.warning(String.format("Returning Link Index for Context Path (%1$s).",
// ServletUriComponentsBuilder.fromCurrentContextPath().build().toString()));
return new LinkIndex()
// Cluster Commands
.add(new Link(CliStrings.STATUS_SHARED_CONFIG, toUri("/services/cluster-config", scheme)))
// Member Commands
.add(new Link(CliStrings.LIST_MEMBER, toUri("/members", scheme)))
.add(new Link(CliStrings.DESCRIBE_MEMBER, toUri("/members/{name}", scheme)))
// Region Commands
.add(new Link(CliStrings.LIST_REGION, toUri("/regions", scheme)))
.add(new Link(CliStrings.DESCRIBE_REGION, toUri("/regions/{name}", scheme)))
.add(new Link(CliStrings.ALTER_REGION, toUri("/regions/{name}", scheme), HttpMethod.PUT))
.add(new Link(CliStrings.CREATE_REGION, toUri("/regions", scheme), HttpMethod.POST))
.add(new Link(CliStrings.DESTROY_REGION, toUri("/regions/{name}", scheme),
HttpMethod.DELETE))
// Index Commands
.add(new Link(CliStrings.LIST_INDEX, toUri("/indexes", scheme)))
.add(new Link(CliStrings.CLEAR_DEFINED_INDEXES, toUri("/indexes?op=clear-defined", scheme),
HttpMethod.DELETE))
.add(new Link(CliStrings.CREATE_INDEX, toUri("/indexes", scheme), HttpMethod.POST))
.add(new Link(CliStrings.CREATE_DEFINED_INDEXES,
toUri("/indexes?op=create-defined", scheme), HttpMethod.POST))
.add(
new Link(CliStrings.DEFINE_INDEX, toUri("/indexes?op=define", scheme), HttpMethod.POST))
.add(new Link(CliStrings.DESTROY_INDEX, toUri("/indexes", scheme), HttpMethod.DELETE))
.add(
new Link(CliStrings.DESTROY_INDEX, toUri("/indexes/{name}", scheme), HttpMethod.DELETE))
// Data Commands
.add(new Link(CliStrings.GET, toUri("/regions/{region}/data", scheme), HttpMethod.GET))
.add(new Link(CliStrings.PUT, toUri("/regions/{region}/data", scheme), HttpMethod.PUT))
.add(
new Link(CliStrings.REMOVE, toUri("/regions/{region}/data", scheme), HttpMethod.DELETE))
.add(new Link(CliStrings.EXPORT_DATA,
toUri("/members/{member}/regions/{region}/data", scheme), HttpMethod.GET))
.add(new Link(CliStrings.IMPORT_DATA,
toUri("/members/{member}/regions/{region}/data", scheme), HttpMethod.POST))
.add(new Link(CliStrings.LOCATE_ENTRY, toUri("/regions/{region}/data/location", scheme),
HttpMethod.GET))
.add(new Link(CliStrings.QUERY, toUri("/regions/data/query", scheme), HttpMethod.GET))
.add(new Link(CliStrings.REBALANCE, toUri("/regions/data?op=rebalance", scheme),
HttpMethod.POST))
// Function Commands
.add(new Link(CliStrings.LIST_FUNCTION, toUri("/functions", scheme)))
.add(new Link(CliStrings.DESTROY_FUNCTION, toUri("/functions/{id}", scheme),
HttpMethod.DELETE))
.add(new Link(CliStrings.EXECUTE_FUNCTION, toUri("/functions/{id}", scheme),
HttpMethod.POST))
// Client Commands
.add(new Link(CliStrings.LIST_CLIENTS, toUri("/clients", scheme)))
.add(new Link(CliStrings.DESCRIBE_CLIENT, toUri("/clients/{clientID}", scheme)))
// Config Commands
.add(new Link(CliStrings.ALTER_RUNTIME_CONFIG, toUri("/config", scheme), HttpMethod.POST))
.add(new Link(CliStrings.DESCRIBE_CONFIG, toUri("/members/{member}/config", scheme)))
.add(new Link(CliStrings.EXPORT_CONFIG, toUri("/config", scheme)))
.add(new Link(CliStrings.EXPORT_SHARED_CONFIG, toUri("/config/cluster", scheme)))
.add(new Link(CliStrings.IMPORT_SHARED_CONFIG, toUri("/config/cluster", scheme),
HttpMethod.POST))
// Deploy Commands
.add(new Link(CliStrings.LIST_DEPLOYED, toUri("/deployed", scheme)))
.add(new Link(CliStrings.DEPLOY, toUri("/deployed", scheme), HttpMethod.POST))
.add(new Link(CliStrings.UNDEPLOY, toUri("/deployed", scheme), HttpMethod.DELETE))
// Disk Store Commands
.add(new Link(CliStrings.LIST_DISK_STORE, toUri("/diskstores", scheme)))
.add(new Link(CliStrings.BACKUP_DISK_STORE, toUri("/diskstores?op=backup", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.COMPACT_DISK_STORE, toUri("/diskstores/{name}?op=compact", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.CREATE_DISK_STORE, toUri("/diskstores", scheme), HttpMethod.POST))
.add(new Link(CliStrings.DESCRIBE_DISK_STORE, toUri("/diskstores/{name}", scheme)))
.add(new Link(CliStrings.DESTROY_DISK_STORE, toUri("/diskstores/{name}", scheme),
HttpMethod.DELETE))
.add(new Link(CliStrings.REVOKE_MISSING_DISK_STORE,
toUri("/diskstores/{id}?op=revoke", scheme), HttpMethod.POST))
.add(new Link(CliStrings.SHOW_MISSING_DISK_STORE, toUri("/diskstores/missing", scheme)))
// Durable Client Commands
.add(new Link(CliStrings.LIST_DURABLE_CQS,
toUri("/durable-clients/{durable-client-id}/cqs", scheme)))
.add(new Link(CliStrings.COUNT_DURABLE_CQ_EVENTS,
toUri("/durable-clients/{durable-client-id}/cqs/events", scheme)))
.add(new Link(CliStrings.COUNT_DURABLE_CQ_EVENTS,
toUri("/durable-clients/{durable-client-id}/cqs/{durable-cq-name}/events", scheme)))
.add(new Link(CliStrings.CLOSE_DURABLE_CLIENTS,
toUri("/durable-clients/{durable-client-id}?op=close", scheme), HttpMethod.POST))
.add(
new Link(CliStrings.CLOSE_DURABLE_CQS,
toUri("/durable-clients/{durable-client-id}/cqs/{durable-cq-name}?op=close",
scheme),
HttpMethod.POST))
// Launcher Lifecycle Commands
.add(new Link(CliStrings.STATUS_LOCATOR, toUri("/members/{name}/locator", scheme)))
.add(new Link(CliStrings.STATUS_SERVER, toUri("/members/{name}/server", scheme)))
// Miscellaneous Commands
.add(new Link(CliStrings.CHANGE_LOGLEVEL, toUri("/groups/{groups}/loglevel", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.CHANGE_LOGLEVEL, toUri("/members/{members}/loglevel", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.CHANGE_LOGLEVEL,
toUri("/members/{members}/groups/{groups}/loglevel", scheme), HttpMethod.POST))
.add(new Link(CliStrings.EXPORT_LOGS, toUri("/logs", scheme)))
.add(new Link(CliStrings.EXPORT_STACKTRACE, toUri("/stacktraces", scheme)))
.add(new Link(CliStrings.GC, toUri("/gc", scheme), HttpMethod.POST))
.add(new Link(CliStrings.GC, toUri("/members/{member}/gc", scheme), HttpMethod.POST))
.add(new Link(CliStrings.NETSTAT, toUri("/netstat", scheme)))
.add(new Link(CliStrings.SHOW_DEADLOCK, toUri("/deadlocks", scheme)))
.add(new Link(CliStrings.SHOW_LOG, toUri("/members/{member}/log", scheme)))
.add(new Link(CliStrings.SHOW_METRICS, toUri("/metrics", scheme)))
.add(new Link(CliStrings.SHUTDOWN, toUri("/shutdown", scheme), HttpMethod.POST)) // verb!
// Queue Commands
.add(new Link(CliStrings.CREATE_ASYNC_EVENT_QUEUE, toUri("/async-event-queues", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.LIST_ASYNC_EVENT_QUEUES, toUri("/async-event-queues", scheme)))
// PDX Commands
.add(new Link(CliStrings.CONFIGURE_PDX, toUri("/pdx", scheme), HttpMethod.POST))
// .add(new Link(CliStrings.PDX_DELETE_FIELD, toUri("/pdx/type/field"), HttpMethod.DELETE))
.add(new Link(CliStrings.PDX_RENAME, toUri("/pdx/type", scheme), HttpMethod.POST))
// Shell Commands
.add(new Link(MBEAN_ATTRIBUTE_LINK_RELATION, toUri("/mbean/attribute", scheme)))
.add(new Link(MBEAN_OPERATION_LINK_RELATION, toUri("/mbean/operation", scheme),
HttpMethod.POST))
.add(new Link(MBEAN_QUERY_LINK_RELATION, toUri("/mbean/query", scheme), HttpMethod.POST))
.add(new Link(PING_LINK_RELATION, toUri("/ping", scheme), HttpMethod.GET))
.add(new Link(CliStrings.VERSION, toUri("/version", scheme)))
// WAN Gateway Commands
.add(new Link(CliStrings.LIST_GATEWAY, toUri("/gateways", scheme)))
.add(new Link(CliStrings.CREATE_GATEWAYRECEIVER, toUri("/gateways/receivers", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.CREATE_GATEWAYSENDER, toUri("/gateways/senders", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.DESTROY_GATEWAYSENDER, toUri("/gateways/senders/{id}", scheme),
HttpMethod.DELETE))
.add(new Link(CliStrings.LOAD_BALANCE_GATEWAYSENDER,
toUri("/gateways/senders/{id}?op=load-balance", scheme), HttpMethod.POST))
.add(new Link(CliStrings.PAUSE_GATEWAYSENDER,
toUri("/gateways/senders/{id}?op=pause", scheme), HttpMethod.POST))
.add(new Link(CliStrings.RESUME_GATEWAYSENDER,
toUri("/gateways/senders/{id}?op=resume", scheme), HttpMethod.POST))
.add(new Link(CliStrings.START_GATEWAYRECEIVER,
toUri("/gateways/receivers?op=start", scheme), HttpMethod.POST))
.add(new Link(CliStrings.START_GATEWAYSENDER, toUri("/gateways/senders?op=start", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.STATUS_GATEWAYRECEIVER, toUri("/gateways/receivers", scheme)))
.add(new Link(CliStrings.STATUS_GATEWAYSENDER, toUri("/gateways/senders/{id}", scheme)))
.add(new Link(CliStrings.STOP_GATEWAYRECEIVER, toUri("/gateways/receivers?op=stop", scheme),
HttpMethod.POST))
.add(new Link(CliStrings.STOP_GATEWAYSENDER,
toUri("/gateways/senders/{id}?op=stop", scheme), HttpMethod.POST));
}
@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}, value = "/ping")
public ResponseEntity<String> ping() {
return new ResponseEntity<String>("<html><body><h1>Mischief Managed!</h1></body></html>",
HttpStatus.OK);
}
@RequestMapping(method = RequestMethod.GET, value = "/version")
@ResponseBody
public String version() {
return GemFireVersion.getProductName().concat("/").concat(GemFireVersion.getGemFireVersion());
}
@RequestMapping(method = RequestMethod.GET, value = "/version/full")
@ResponseBody
public String versionSimple() {
return GemFireVersion.asString();
}
}