/*
* RHQ Management Platform
* Copyright (C) 2005-2012 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.server.rest;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiError;
import com.wordnik.swagger.annotations.ApiErrors;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import org.jboss.resteasy.annotations.GZIP;
import org.jboss.resteasy.annotations.cache.Cache;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.core.domain.criteria.ResourceOperationHistoryCriteria;
import org.rhq.core.domain.operation.JobId;
import org.rhq.core.domain.operation.OperationDefinition;
import org.rhq.core.domain.operation.OperationRequestStatus;
import org.rhq.core.domain.operation.ResourceOperationHistory;
import org.rhq.core.domain.operation.bean.ResourceOperationSchedule;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.domain.util.PageOrdering;
import org.rhq.core.domain.util.StringUtils;
import org.rhq.enterprise.server.operation.OperationDefinitionNotFoundException;
import org.rhq.enterprise.server.operation.OperationManagerLocal;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.enterprise.server.resource.ResourceNotFoundException;
import org.rhq.enterprise.server.rest.domain.Link;
import org.rhq.enterprise.server.rest.domain.OperationDefinitionRest;
import org.rhq.enterprise.server.rest.domain.OperationHistoryRest;
import org.rhq.enterprise.server.rest.domain.OperationRest;
import org.rhq.enterprise.server.rest.domain.SimplePropDef;
import org.rhq.enterprise.server.rest.helper.ConfigurationHelper;
/**
* Deal with operations
* @author Heiko W. Rupp
*/
@Path("/operation")
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@Stateless
@Interceptors(SetCallerInterceptor.class)
@Api(value = "Endpoints for operations.",
description = "These endpoints deal with scheduling of operations and retrieval of operation results. ")
public class OperationsHandlerBean extends AbstractRestBean {
@EJB
private OperationManagerLocal opsManager;
@EJB
private ResourceManagerLocal resourceManager;
@GET
@Path("definition/{id}")
@Cache(maxAge = 1200)
@ApiOperation("Retrieve a single operation definition by its id")
public Response getOperationDefinition(
@ApiParam("Id of the definition to retrieve") @PathParam("id") int definitionId,
@ApiParam("Id of a resource that supports this operation") @QueryParam("resourceId") Integer resourceId,
@Context UriInfo uriInfo,
@Context Request request) {
OperationDefinition def;
def = getFromCache(definitionId, OperationDefinition.class);
if (def==null) {
try {
def = opsManager.getOperationDefinition(caller,definitionId);
putToCache(definitionId,OperationDefinition.class,def);
}
catch (OperationDefinitionNotFoundException ode) {
throw new StuffNotFoundException("Operation definition with id " + definitionId);
}
}
EntityTag eTag = new EntityTag(Integer.toHexString(def.hashCode()));
Response.ResponseBuilder builder = request.evaluatePreconditions(eTag);
if (builder==null) {
OperationDefinitionRest odr = new OperationDefinitionRest();
odr.setId(def.getId());
odr.setName(def.getName());
copyParamsForDefinition(def, odr);
builder=Response.ok(odr);
// Add some links
if (resourceId!=null) {
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/operation/definition/{id}");
uriBuilder.queryParam("resourceId",resourceId);
Link createLink = new Link("create",uriBuilder.build(definitionId).toString());
odr.addLink(createLink);
}
}
builder.tag(eTag);
return builder.build();
}
@GZIP
@GET
@Path("definitions")
@Cache(maxAge = 1200)
@ApiOperation("List all operation definitions for a resource")
public Response getOperationDefinitions(
@ApiParam(value = "Id of the resource", required = true) @QueryParam("resourceId") Integer resourceId,
@Context UriInfo uriInfo,
@Context Request request) {
if (resourceId == null) {
throw new ParameterMissingException("resourceId");
}
Resource res;
try {
res = resourceManager.getResource(caller,resourceId);
}
catch (ResourceNotFoundException rnfe) {
throw new StuffNotFoundException("resource with id " + resourceId);
}
ResourceType resourceType = res.getResourceType();
EntityTag eTag = new EntityTag(Integer.toHexString(resourceType.hashCode()));
Response.ResponseBuilder builder = request.evaluatePreconditions(eTag);
if (builder==null) {
Set<OperationDefinition> opDefList = resourceType.getOperationDefinitions();
List<OperationDefinitionRest> resultList = new ArrayList<OperationDefinitionRest>(opDefList.size());
for (OperationDefinition def : opDefList) {
putToCache(def.getId(),OperationDefinition.class,def);
OperationDefinitionRest odr = new OperationDefinitionRest();
odr.setId(def.getId());
odr.setName(def.getName());
copyParamsForDefinition(def,odr);
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/operation/definition/{id}");
uriBuilder.queryParam("resourceId",resourceId);
Link createLink = new Link("create",uriBuilder.build(def.getId()).toString());
odr.addLink(createLink);
resultList.add(odr);
}
GenericEntity<List<OperationDefinitionRest>> entity = new GenericEntity<List<OperationDefinitionRest>>(resultList){};
builder = Response.ok(entity);
}
builder.tag(eTag);
return builder.build();
}
@POST
@Path("definition/{id}")
@ApiOperation("Create a new (draft) operation from the passed definition id for the passed resource")
public Response createOperation(
@ApiParam("Id of the definition") @PathParam("id") int definitionId,
@ApiParam(value = "Id of the resource", required = true) @QueryParam("resourceId") Integer resourceId,
@Context UriInfo uriInfo) {
if (resourceId == null) {
throw new ParameterMissingException("resourceId");
}
try {
// Check if the resource exists at all
resourceManager.getResource(caller,resourceId);
}
catch (ResourceNotFoundException rnfe) {
throw new StuffNotFoundException("resource with id " + resourceId);
}
OperationDefinition opDef;
try {
opDef = opsManager.getOperationDefinition(caller,definitionId);
}
catch (OperationDefinitionNotFoundException odnfe) {
throw new StuffNotFoundException("Operation definition with id " + definitionId);
}
OperationRest operationRest = new OperationRest(resourceId,definitionId);
operationRest.setId((int)System.currentTimeMillis()); // TODO better id (?)(we need one for pUT later on)
operationRest.setReadyToSubmit(false);
operationRest.setName(opDef.getName());
ConfigurationDefinition paramDefinition = opDef.getParametersConfigurationDefinition();
if (paramDefinition != null) {
for (PropertyDefinition propDefs : paramDefinition.getNonGroupedProperties()) { // TODO extend to all properties ?
operationRest.getParams().put(propDefs.getName(),"TODO"); // TODO type and value of the value
}
}
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/operation/{id}");
URI uri = uriBuilder.build(operationRest.getId());
Link editLink = new Link("edit",uri.toString());
operationRest.addLink(editLink);
Response.ResponseBuilder builder = Response.ok(operationRest);
putToCache(operationRest.getId(),OperationRest.class,operationRest);
return builder.build();
}
@GET
@Path("{id}")
@ApiOperation("Return a (draft) operation")
public Response getOperation(@ApiParam("Id of the operation to retrieve") @PathParam("id") int operationId) {
OperationRest op = getFromCache(operationId,OperationRest.class);
if (op==null) {
throw new StuffNotFoundException("Operation with id " + operationId);
}
return Response.ok(op).build();
}
@PUT
@Path("{id}")
@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@ApiOperation("Update a (draft) operation. If the state is set to 'ready', the operation will be scheduled")
@ApiErrors({
@ApiError(code = 404, reason = "No draft operation with the passed id exists"),
@ApiError(code = 406, reason = "Draft was set for scheduling, but parameters failed validation"),
@ApiError(code = 200, reason = "Update was successful, operation was scheduled if requested" )
}
)
public Response updateOperation(@ApiParam("Id of the operation to update") @PathParam("id") int operationId,
OperationRest operation, @Context UriInfo uriInfo) {
OperationRest op = getFromCache(operationId,OperationRest.class);
if (op==null) {
throw new StuffNotFoundException("Operation with id " + operationId);
}
Configuration parameters = ConfigurationHelper.mapToConfiguration(operation.getParams());
if (operation.isReadyToSubmit()) {
OperationDefinition opDef = opsManager.getOperationDefinition(caller,operation.getDefinitionId());
// Validate parameters
ConfigurationDefinition parameterDefinition = opDef.getParametersConfigurationDefinition();
if (parameterDefinition!=null) {
// There are parameters defined, so lets validate them.
List<String> errorMessages = ConfigurationHelper.checkConfigurationWrtDefinition(parameters, parameterDefinition);
if (errorMessages.size()>0) {
// Configuration is not ok
operation.setReadyToSubmit(false);
throw new BadArgumentException("Validation of parameters failed", StringUtils.getListAsString(errorMessages,", "));
}
}
}
if (operation.isReadyToSubmit()) {
// submit
ResourceOperationSchedule sched = opsManager.scheduleResourceOperation(caller,operation.getResourceId(),operation.getName(),0,0,0,-1,
parameters,"Test");
JobId jobId = new JobId(sched.getJobName(),sched.getJobGroup());
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/operation/history/{id}");
URI uri = uriBuilder.build(jobId);
Link histLink = new Link("history",uri.toString());
operation.addLink(histLink);
}
else {
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/operation/{id}");
URI uri = uriBuilder.build(operationId);
Link editLink = new Link("edit",uri.toString());
operation.addLink(editLink);
}
// Update item in cache
putToCache(operationId,OperationRest.class,operation);
Response.ResponseBuilder builder = Response.ok(operation);
return builder.build();
}
@DELETE
@Path("{id}")
@ApiOperation("Delete a (draft) operation")
public Response cancelOperation(@ApiParam("Id of the operation to remove") @PathParam("id") int operationId) {
log.info("Cancel called");
removeFromCache(operationId,OperationRest.class);
return null; // TODO: Customise this generated block
}
@GZIP
@GET
@Path("history/{id}")
@ApiOperation("Return the outcome of the scheduled operation")
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_HTML})
public Response outcome(
@ApiParam("Name of the submitted job.") @PathParam("id") String jobName,
@Context UriInfo uriInfo,
@Context HttpHeaders httpHeaders) {
MediaType mediaType = httpHeaders.getAcceptableMediaTypes().get(0);
ResourceOperationHistoryCriteria criteria = new ResourceOperationHistoryCriteria();
JobId jobId = new JobId(jobName);
criteria.addFilterJobId(jobId);
ResourceOperationHistory history ;//= opsManager.getOperationHistoryByJobId(caller,jobName);
List<ResourceOperationHistory> list = opsManager.findResourceOperationHistoriesByCriteria(caller,criteria);
if (list==null || list.isEmpty()) {
log.info("No history with id " + jobId + " found");
throw new StuffNotFoundException("OperationHistory with id " + jobId);
}
history = list.get(0);
OperationHistoryRest hist = historyToHistoryRest(history, uriInfo);
Response.ResponseBuilder builder;
if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) {
builder = Response.ok(renderTemplate("operationHistory.ftl",hist));
} else {
builder = Response.ok(hist);
}
if (history.getStatus()== OperationRequestStatus.SUCCESS) {
// add a long time cache header
CacheControl cc = new CacheControl();
cc.setMaxAge(1200);
builder.cacheControl(cc);
}
return builder.build();
}
@GZIP
@GET
@Path("history")
@ApiOperation("Return the outcome of the executed operations for a resource")
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_HTML})
public Response listHistory(
@ApiParam("Id of a resource to limit to") @QueryParam("resourceId") int resourceId,
@ApiParam("Page size for paging") @QueryParam("ps") @DefaultValue("20") int pageSize,
@ApiParam("Page for paging, 0-based") @QueryParam("page") Integer page,
@Context UriInfo uriInfo,
@Context HttpHeaders httpHeaders) {
ResourceOperationHistoryCriteria criteria = new ResourceOperationHistoryCriteria();
criteria.addSortStartTime(PageOrdering.ASC);
if (resourceId>0) {
criteria.addFilterResourceIds(resourceId);
}
if (page!=null) {
criteria.setPaging(page,pageSize);
}
criteria.addSortEndTime(PageOrdering.DESC);
PageList<ResourceOperationHistory> histories = opsManager.findResourceOperationHistoriesByCriteria(caller, criteria);
List<OperationHistoryRest> result = new ArrayList<OperationHistoryRest>();
for (ResourceOperationHistory roh : histories) {
OperationHistoryRest historyRest = historyToHistoryRest(roh,uriInfo);
result.add(historyRest);
}
MediaType mediaType = httpHeaders.getAcceptableMediaTypes().get(0);
Response.ResponseBuilder builder = Response.ok();
builder.type(mediaType);
if (mediaType.equals(MediaType.TEXT_HTML_TYPE)) {
builder.entity(renderTemplate("listOperationHistory.ftl", result));
} else if (mediaType.equals(wrappedCollectionJsonType)) {
wrapForPaging(builder,uriInfo,histories,result);
} else {
GenericEntity<List<OperationHistoryRest>> res = new GenericEntity<List<OperationHistoryRest>>(result) {};
builder.entity(res);
createPagingHeader(builder,uriInfo,histories);
}
return builder.build();
}
@DELETE
@Path("history/{id}")
@ApiOperation(value = "Delete the operation history item with the passed id", notes = "This operation is by default idempotent, returning 204." +
"If you want to check if the job existed at all, you need to pass the 'validate' query parameter.")
@ApiErrors({
@ApiError(code = 204, reason = "Item was deleted or did not exist with validation not set"),
@ApiError(code = 404, reason = "Item did not exist and validate was set"),
@ApiError(code = 406, reason = "Passed Job ID did not pass name validation")
})
public Response deleteOperationHistoryItem(@ApiParam("Name fo the submitted job") @PathParam("id") String jobName,
@ApiParam("Validate if the job exists") @QueryParam("validate") @DefaultValue("false") boolean validate) {
ResourceOperationHistoryCriteria criteria = new ResourceOperationHistoryCriteria();
JobId filterJobId;
try {
filterJobId = new JobId(jobName);
} catch (Exception e) {
// jobName most likely did not match the expected format
throw new BadArgumentException("jobName","Does not match the format for job history items");
}
criteria.addFilterJobId(filterJobId);
criteria.clearPaging();//disable paging as the code assumes all the results will be returned.
List<ResourceOperationHistory> list = opsManager.findResourceOperationHistoriesByCriteria(caller,criteria);
if ((list != null && !list.isEmpty())) {
ResourceOperationHistory history = list.get(0);
opsManager.deleteOperationHistory(caller,history.getId(),false);
}
else {
if (validate) {
throw new StuffNotFoundException("Job with id " + jobName);
}
}
return Response.noContent().build();
}
/**
* Create a REST-object from the passed operation history
* @param history History object to convert
* @param uriInfo URI info of the incoming request, used to create links
* @return a populated OperationHistoryRest object
*/
private OperationHistoryRest historyToHistoryRest(ResourceOperationHistory history, UriInfo uriInfo) {
String status;
if (history.getStatus()==null) {
status = " - no information yet -";
}
else {
status = history.getStatus().getDisplayName();
}
OperationHistoryRest hist = new OperationHistoryRest();
hist.setStatus(status);
if (history.getResource()!=null) {
hist.setResourceName(history.getResource().getName());
}
hist.setOperationName(history.getOperationDefinition().getName());
hist.lastModified(history.getModifiedTime());
if (history.getErrorMessage()!=null) {
hist.setErrorMessage(history.getErrorMessage());
}
if (history.getResults()!=null) {
Configuration results = history.getResults();
for (Property p : results.getProperties()) {
String val;
if (p instanceof PropertySimple) {
val = ((PropertySimple)p).getStringValue();
}
else {
val = p.toString();
}
hist.getResult().put(p.getName(),val);
}
}
String jobName = history.getJobName();
String jobGroup = history.getJobGroup();
JobId jobId = new JobId(jobName, jobGroup);
hist.setJobId(jobId.toString());
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/operation/history/{id}");
URI url = uriBuilder.build(jobId);
Link self = new Link("self",url.toString());
hist.getLinks().add(self);
return hist;
}
/**
* Copies the parameters of an OperationDefinition into to an object that can be
* returned to a REST-client, so that this knows which fields are to be filled in,
* of which type they are and which ones are required
* @param def OperationsDefinition to "copy"
* @param definitionRest The definition to fill in
*/
private void copyParamsForDefinition(OperationDefinition def, OperationDefinitionRest definitionRest) {
ConfigurationDefinition cd = def.getParametersConfigurationDefinition();
if (cd==null) {
return;
}
for (Map.Entry<String,PropertyDefinition> entry : cd.getPropertyDefinitions().entrySet()) {
PropertyDefinition pd = entry.getValue();
if (pd instanceof PropertyDefinitionSimple) {
PropertyDefinitionSimple pds = (PropertyDefinitionSimple) pd;
SimplePropDef prop = new SimplePropDef();
prop.setName(pds.getName());
prop.setRequired(pds.isRequired());
prop.setType(pds.getType());
prop.setDefaultValue(pds.getDefaultValue());
definitionRest.addParam(prop);
}
log.debug("copyParams: " + pd.getName() + " not yet supported");
}
}
}