package com.azaptree.services.command.http.handler;
/*
* #%L
* AZAPTREE-COMMAND-HTTP-SERVICE
* %%
* Copyright (C) 2012 - 2013 AZAPTREE.COM
* %%
* Licensed 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.
* #L%
*/
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.apache.commons.chain.Command;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import com.azaptree.services.command.CommandCatalog;
import com.azaptree.services.command.CommandKey;
import com.azaptree.services.command.CommandService;
import com.azaptree.services.command.http.WebCommandContext;
import com.azaptree.services.command.http.WebRequestCommand;
import com.azaptree.services.http.handler.AsyncSuspendContinueHttpHandlerSupport;
import com.azaptree.services.http.headers.ResponseMessageHeaders;
import com.azaptree.wadl.Application;
import com.azaptree.wadl.Doc;
import com.azaptree.wadl.Grammars;
import com.azaptree.wadl.Include;
import com.azaptree.wadl.Method;
import com.azaptree.wadl.Representation;
import com.azaptree.wadl.Resource;
import com.azaptree.wadl.Resources;
import com.azaptree.wadl.Response;
import com.google.common.collect.ImmutableMap;
/**
* REST API:
*
* <code>
* GET /command-service.wadl returns WADL which describes the REST API
* GET /command-service/{catalog}/{command}.xsd returns command XSD, which describes the command's XML messages
* POST /command-service/{catalog}/{command} executes the command - the command XML request message is provided in the HTTP request body
* </code>
*
* @author alfio
*
*/
public class CommandServiceHandler extends AsyncSuspendContinueHttpHandlerSupport {
@Autowired
private CommandService commandService;
private Map<String /* target uri */, CommandKey> targetUriCommandKeyMap;
private final String applicationHttpUrlBase;
/**
*
* @param executor
* @param continuationTimeoutMillis
* @param applicationHttpUrlBase
* used when generating the WADL to specify the base HTTP url for <code>/application/resources/@base</code>, e.g.
* http://localhost:8080/command-service/
*/
public CommandServiceHandler(final Executor executor, final long continuationTimeoutMillis, final String applicationHttpUrlBase) {
super(executor, continuationTimeoutMillis);
Assert.hasText(applicationHttpUrlBase, "applicationHttpUrlBase is required");
this.applicationHttpUrlBase = applicationHttpUrlBase;
}
/**
*
* @param executor
* @param applicationHttpUrlBase
* used when generating the WADL to specify the base HTTP url for <code>/application/resources/@base</code>, e.g.
* http://localhost:8080/command-service/
*/
public CommandServiceHandler(final Executor executor, final String applicationHttpUrlBase) {
super(executor);
Assert.hasText(applicationHttpUrlBase, "applicationHttpUrlBase is required");
this.applicationHttpUrlBase = applicationHttpUrlBase;
}
private Grammars createWadlGrammars() {
final Grammars grammars = new Grammars();
final List<CommandKey> keys = new ArrayList<>(targetUriCommandKeyMap.values());
Collections.sort(keys);
for (final CommandKey commandKey : keys) {
final WebRequestCommand<?, ?> command = (WebRequestCommand<?, ?>) commandService.getCommand(commandKey);
if (command.hasXmlSchema()) {
final Include include = new Include();
include.setHref(String.format("%s/command-service/%s/%s.xsd", applicationHttpUrlBase, commandKey.getCatalogName(), commandKey.getCommandName()));
grammars.getInclude().add(include);
}
}
return grammars;
}
private Resources createWadlResources(final String catalogName) {
final Resources resources = new Resources();
resources.setBase(String.format("%s/command-service/%s/", applicationHttpUrlBase, catalogName));
final CommandCatalog catalog = commandService.getCommandCatalog(catalogName);
for (final String commandName : catalog.getCommandNames()) {
final WebRequestCommand<?, ?> command = (WebRequestCommand<?, ?>) commandService.getCommand(new CommandKey(catalogName, commandName));
resources.getResource().add(command.createCommandResourceWadl());
if (command.hasXmlSchema()) {
final Resource resource = new Resource();
resource.setPath(commandName + ".xsd");
resources.getResource().add(resource);
final Doc doc = new Doc();
doc.getContent().add("The command's XML schema is returned");
resource.getDoc().add(doc);
final Method method = new Method();
method.setName("GET");
resource.getMethodOrResource().add(method);
final Response response = new Response();
method.getResponse().add(response);
response.getStatus().add(Long.valueOf(HttpStatus.OK_200));
final Representation representation = new Representation();
representation.setMediaType("application/xml");
response.getRepresentation().add(representation);
}
}
return resources;
}
protected WebCommandContext<?, ?> createWebCommandContext(final WebRequestCommand<?, ?> command, final Request baseRequest,
final HttpServletRequest request,
final HttpServletResponse response) throws JAXBException, IOException {
if (command.getRequestClass().isPresent()) {
final JAXBContext ctx = command.getJaxbContext().get();
final Object requestMessage;
final Unmarshaller unmarshaller = ctx.createUnmarshaller();
requestMessage = unmarshaller.unmarshal(baseRequest.getInputStream());
if (requestMessage instanceof JAXBElement) {
return new WebCommandContext<>(request, response, ((JAXBElement<?>) requestMessage).getValue());
}
return new WebCommandContext<>(request, response, requestMessage);
}
return new WebCommandContext<>(request, response);
}
private void generateCommandXSD(final String target, final HttpServletResponse response) throws IOException {
final CommandKey commandKey = targetUriCommandKeyMap.get(toCommandUriTarget(target));
final CommandCatalog catalog = commandService.getCommandCatalog(commandKey.getCatalogName());
@SuppressWarnings("rawtypes")
final WebRequestCommand command = (WebRequestCommand) catalog.getCommand(commandKey.getCommandName());
if (!command.hasXmlSchema()) {
response.setStatus(HttpStatus.NO_CONTENT_204);
return;
}
response.setStatus(HttpStatus.OK_200);
response.setContentType("application/xml");
response.setCharacterEncoding("UTF-8");
command.generateSchema(response.getOutputStream());
}
private void generateWADL(final Request baseRequest, final HttpServletResponse response) {
// TODO: implement HTTP caching
response.setStatus(HttpStatus.OK_200);
response.setContentType("application/vnd.sun.wadl+xml");
final Application app = new Application();
app.setGrammars(createWadlGrammars());
for (String catalogName : commandService.getCommandCatalogNames()) {
app.getResources().add(createWadlResources(catalogName));
}
try {
final JAXBContext wadlJaxbContext = JAXBContext.newInstance(Application.class.getPackage().getName());
final Marshaller marshaller = wadlJaxbContext.createMarshaller();
marshaller.marshal(app, response.getOutputStream());
} catch (JAXBException | IOException e) {
throw new IllegalStateException("Failed to marshall WADL to HTTP response output stream", e);
}
baseRequest.setHandled(true);
}
private void handleCommandExecutionError(final Request baseRequest, final HttpServletResponse response, final Exception exception) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
final StringBuilder sb = new StringBuilder("Failed to execute command : ").append(exception.getMessage());
response.setHeader(ResponseMessageHeaders.STATUS_MSG.header, sb.toString());
baseRequest.setHandled(true);
}
private void handleGET(final String target, final Request baseRequest, final HttpServletResponse response)
throws IOException {
if (StringUtils.equals(target, "/command-service.wadl")) {
generateWADL(baseRequest, response);
baseRequest.setHandled(true);
} else if (targetUriCommandKeyMap.containsKey(toCommandUriTarget(target))) {
generateCommandXSD(target, response);
baseRequest.setHandled(true);
} else {
handleInvalidUri(baseRequest, response);
}
}
private void handleInvalidUri(final Request baseRequest, final HttpServletResponse response) {
response.setStatus(HttpStatus.NOT_FOUND_404);
baseRequest.setHandled(true);
}
private void handleMethodNotAllowed(final Request baseRequest, final HttpServletResponse response) {
response.setStatus(HttpStatus.METHOD_NOT_ALLOWED_405);
response.setHeader(HttpHeaders.ALLOW, "GET, POST");
baseRequest.setHandled(true);
}
private void handlePOST(final String target, final Request baseRequest, final HttpServletResponse response) {
if (!targetUriCommandKeyMap.containsKey(target)) {
handleInvalidUri(baseRequest, response);
}
}
private void handleUnmarshallingRequestMessageError(final Request baseRequest, final HttpServletResponse response, final Exception exception) {
response.setStatus(HttpStatus.BAD_REQUEST_400);
final StringBuilder sb = new StringBuilder("Failed to unmarshall request message : ").append(exception.getMessage());
response.setHeader(ResponseMessageHeaders.STATUS_MSG.header, sb.toString());
baseRequest.setHandled(true);
}
@PostConstruct
private void init() {
final Map<String, CommandKey> map = new HashMap<>();
final String[] catalogNames = commandService.getCommandCatalogNames();
for (final String catalogName : catalogNames) {
final CommandCatalog catalog = commandService.getCommandCatalog(catalogName);
final String[] commandNames = catalog.getCommandNames();
for (final String command : commandNames) {
final Command cmd = catalog.getCommand(command);
if (cmd instanceof WebRequestCommand) {
final String uri = String.format("/command-service/%s/%s", catalogName, command);
final CommandKey key = new CommandKey(catalogName, command);
map.put(uri, key);
log.info("WebRequestCommand uri: {}", uri);
}
}
}
targetUriCommandKeyMap = ImmutableMap.<String, CommandKey> builder().putAll(map).build();
}
@Override
protected void preProcess(final String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
final String method = baseRequest.getMethod();
if ("GET".equals(method)) {
handleGET(target, baseRequest, response);
} else if ("POST".equals(method)) {
handlePOST(target, baseRequest, response);
} else {
handleMethodNotAllowed(baseRequest, response);
}
}
@SuppressWarnings("rawtypes")
@Override
protected void process(final String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) {
final CommandKey commandKey = targetUriCommandKeyMap.get(target);
final WebRequestCommand command = (WebRequestCommand) commandService.getCommand(commandKey);
final WebCommandContext<?, ?> commandContext;
try {
commandContext = createWebCommandContext(command, baseRequest, request, response);
} catch (JAXBException | IOException e) {
log.error(String.format("%s : Failed to unmarshal request message", target), e);
handleUnmarshallingRequestMessageError(baseRequest, response, e);
return;
}
try {
command.execute(commandContext);
} catch (final Exception e) {
log.error(String.format("%s : Failed to execute command : %s", target, commandKey), e);
handleCommandExecutionError(baseRequest, response, e);
}
}
private String toCommandUriTarget(final String target) {
if (target.endsWith(".xsd")) {
return target.substring(0, target.length() - 4);
}
return target;
}
}