/*
* Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library 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 Lesser General Public License for more
* details.
*/
package org.entando.entando.aps.system.services.api.server;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.servlet.ServletContext;
import javax.ws.rs.core.Response;
import org.entando.entando.aps.system.services.api.IApiCatalogManager;
import org.entando.entando.aps.system.services.api.IApiErrorCodes;
import org.entando.entando.aps.system.services.api.model.AbstractApiResponse;
import org.entando.entando.aps.system.services.api.model.ApiError;
import org.entando.entando.aps.system.services.api.model.ApiException;
import org.entando.entando.aps.system.services.api.model.ApiMethod;
import org.entando.entando.aps.system.services.api.model.ApiMethodParameter;
import org.entando.entando.aps.system.services.api.model.ApiMethodResult;
import org.entando.entando.aps.system.services.api.model.StringApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.io.Resource;
import org.springframework.web.context.ServletContextAware;
import com.agiletec.aps.system.common.renderer.IVelocityRenderer;
import com.agiletec.aps.system.exception.ApsSystemException;
import com.agiletec.aps.util.ApsWebApplicationUtils;
import com.agiletec.aps.util.FileTextReader;
/**
* @author E.Santoboni
*/
public class ResponseBuilder implements IResponseBuilder, BeanFactoryAware, ServletContextAware {
private static final Logger _logger = LoggerFactory.getLogger(ApiRestStatusServer.class);
@Override
@Deprecated
public Object createResponse(String resourceName, Properties parameters) throws ApsSystemException {
Object apiResponse = null;
try {
ApiMethod method = this.extractApiMethod(ApiMethod.HttpMethod.GET, null, resourceName);
apiResponse = this.createResponse(method, parameters);
} catch (ApiException e) {
_logger.error("Error creating response for method GET, resource '{}'", resourceName, e);
if (apiResponse == null) {
apiResponse = new StringApiResponse();
}
((AbstractApiResponse) apiResponse).addErrors(e.getErrors());
}
return apiResponse;
}
@Override
public Object createResponse(ApiMethod method, Properties parameters) throws ApsSystemException {
return createResponse(method, null, parameters);
}
@Override
public Object createResponse(ApiMethod method, Object bodyObject, Properties parameters) throws ApsSystemException {
AbstractApiResponse response = null;
try {
this.checkParameter(method, parameters);
Object bean = this.extractBean(method);
Object masterResult = null;
if (method.getHttpMethod().equals(ApiMethod.HttpMethod.GET)) {
masterResult = this.invokeGetMethod(method, bean, null, parameters, true);
if (null == masterResult) {
ApiError error = new ApiError(IApiErrorCodes.API_INVALID_RESPONSE, "Invalid or null Response", Response.Status.SERVICE_UNAVAILABLE);
throw new ApiException(error);
}
} else {
masterResult = this.invokePutPostDeleteMethod(method, bean, parameters, bodyObject);
}
if (null == method.getResponseClassName()) {
return masterResult;
}
response = this.buildApiResponseObject(method);
if (null == response && (masterResult instanceof String)) {
return masterResult;
}
String htmlResult = this.extractHtmlResult(masterResult, response, method, parameters, bean);
if (masterResult instanceof ApiMethodResult) {
response.addErrors(((ApiMethodResult) masterResult).getErrors());
response.setResult(((ApiMethodResult) masterResult).getResult(), htmlResult);
} else {
response.setResult(masterResult, htmlResult);
}
} catch (ApiException e) {
if (response == null) {
response = new StringApiResponse();
}
response.addErrors(e.getErrors());
response.setResult(FAILURE, null);
} catch (Throwable t) {
_logger.error("Error creating response - {}", this.buildApiSignature(method), t);
String message = "Error creating response - " + this.buildApiSignature(method);
if (response == null) {
response = new StringApiResponse();
}
ApiError error = new ApiError(IApiErrorCodes.API_METHOD_ERROR, message, Response.Status.INTERNAL_SERVER_ERROR);
response.addError(error);
response.setResult(FAILURE, null);
}
return response;
}
private String extractHtmlResult(Object masterResult,
AbstractApiResponse apiResponse, ApiMethod apiMethod, Properties parameters, Object bean) {
String htmlResult = null;
try {
htmlResult = (String) this.invokeGetMethod(apiMethod, bean, "ToHtml", parameters, false);
if (null != htmlResult) {
return htmlResult;
}
String template = this.extractTemplate(apiMethod);
if (null == template) {
return null;
}
htmlResult = this.getVelocityRenderer().render(masterResult, template);
} catch (ApiException t) {
if (null != t.getErrors()) {
apiResponse.addErrors(t.getErrors());
} else {
_logger.error("Error creating html response - {}", this.buildApiSignature(apiMethod), t);
}
} catch (Throwable t) {
_logger.error("Error creating html response - {}", this.buildApiSignature(apiMethod), t);
}
return htmlResult;
}
protected String extractTemplate(ApiMethod apiMethod) throws Exception {
String template = null;
InputStream is = null;
try {
StringBuilder path = new StringBuilder("classpath*:/api/");
if (null != apiMethod.getPluginCode()) {
path.append("plugins/").append(apiMethod.getPluginCode()).append("/");
} else if (!apiMethod.getSource().equalsIgnoreCase("core")) {
path.append(apiMethod.getSource()).append("/");
}
path.append("aps/get/");
if (null != apiMethod.getNamespace()) {
path.append(apiMethod.getNamespace()).append("/");
}
path.append(apiMethod.getResourceName()).append("/description-item.vm");
Resource[] resources = ApsWebApplicationUtils.getResources(path.toString(), this.getServletContext());
if (null != resources && resources.length == 1) {
Resource resource = resources[0];
is = resource.getInputStream();
}
if (null == is) {
_logger.debug("Null Input Stream - template file path {}", path.toString());
return null;
}
template = FileTextReader.getText(is);
} catch (Throwable t) {
_logger.error("Error extracting template - {}", this.buildApiSignature(apiMethod), t);
} finally {
if (null != is) {
is.close();
}
}
return template;
}
private void checkParameter(ApiMethod apiMethod, Properties parameters) throws ApiException, Throwable {
try {
List<ApiMethodParameter> apiParameters = apiMethod.getParameters();
if (null == apiParameters || apiParameters.isEmpty()) {
return;
}
List<ApiError> errors = new ArrayList<ApiError>();
for (int i = 0; i < apiParameters.size(); i++) {
ApiMethodParameter apiParam = apiParameters.get(i);
String paramName = apiParam.getKey();
Object value = parameters.get(paramName);
if (apiParam.isRequired() && (null == value || value.toString().trim().length() == 0)) {
errors.add(new ApiError(IApiErrorCodes.API_PARAMETER_REQUIRED, "Parameter '" + paramName + "' is required", Response.Status.BAD_REQUEST));
}
}
if (!errors.isEmpty()) {
throw new ApiException(errors);
}
} catch (ApiException t) {
throw t;
} catch (Throwable t) {
_logger.error("Error checking api parameters", t);
throw new ApsSystemException("Internal Error", t);
}
}
private AbstractApiResponse buildApiResponseObject(ApiMethod apiMethod) throws ApiException {
AbstractApiResponse apiResponse = null;
try {
Class responseClass = Class.forName(apiMethod.getResponseClassName());
apiResponse = (AbstractApiResponse) responseClass.newInstance();
} catch (Exception e) {
_logger.error("Error creating instance of response '{}'", apiMethod.getResponseClassName(), e);
}
return apiResponse;
}
@Override
@Deprecated
public Object invoke(String resourceName, Properties parameters) throws ApiException, ApsSystemException {
Object result = null;
try {
ApiMethod api = this.extractApiMethod(ApiMethod.HttpMethod.GET, null, resourceName);
this.checkParameter(api, parameters);
Object bean = this.extractBean(api);
result = this.invokeGetMethod(api, bean, "", parameters, true);
} catch (ApiException ae) {
_logger.error("Error invoking method GET for resource '{}'", resourceName, ae);
throw ae;
} catch (Throwable t) {
_logger.error("Error invoking method GET for resource '{}'", resourceName, t);
throw new ApsSystemException("Error invoking method GET for resource '" + resourceName + "'", t);
}
return result;
}
@Override
public ApiMethod extractApiMethod(ApiMethod.HttpMethod httpMethod, String namespace, String resourceName) throws ApiException {
ApiMethod api = null;
String signature = this.buildApiSignature(httpMethod, namespace, resourceName);
try {
api = this.getApiCatalogManager().getMethod(httpMethod, namespace, resourceName);
if (null == api) {
ApiError error = new ApiError(IApiErrorCodes.API_INVALID, signature + " does not exists", Response.Status.NOT_FOUND);
throw new ApiException(error);
}
if (!api.isActive()) {
ApiError error = new ApiError(IApiErrorCodes.API_INVALID, signature + " does not exists", Response.Status.NOT_FOUND);
throw new ApiException(error);
}
} catch (ApiException ae) {
_logger.error("Error extracting api method {}", this.buildApiSignature(httpMethod, namespace, resourceName), ae);
throw ae;
} catch (Throwable t) {
_logger.error("Error extracting api method {}", this.buildApiSignature(httpMethod, namespace, resourceName), t);
throw new ApiException(IApiErrorCodes.SERVER_ERROR, signature + " is not supported", Response.Status.INTERNAL_SERVER_ERROR);
}
return api;
}
private String buildApiSignature(ApiMethod apiMethod) {
return this.buildApiSignature(apiMethod.getHttpMethod(), apiMethod.getNamespace(), apiMethod.getResourceName());
}
private String buildApiSignature(ApiMethod.HttpMethod httpMethod, String namespace, String resourceName) {
StringBuilder buffer = new StringBuilder();
buffer.append("Method '").append(httpMethod.toString()).append("' Resource '").append(resourceName).append("'");
if (null != namespace) {
buffer.append(" Namespace '").append(namespace).append("'");
}
return buffer.toString();
}
protected Object extractBean(ApiMethod api) throws ApsSystemException, ApiException {
Object bean = this.getBeanFactory().getBean(api.getSpringBean());
if (null == bean) {
_logger.error("Null bean '{}' for api {}", api.getSpringBean(), this.buildApiSignature(api));
throw new ApiException(IApiErrorCodes.SERVER_ERROR, this.buildApiSignature(api) + " is not supported", Response.Status.INTERNAL_SERVER_ERROR);
}
return bean;
}
@Deprecated
protected Object invokeMethod(ApiMethod api, Object bean,
String methodSuffix, Properties parameters, boolean throwException) throws ApiException, Throwable {
return this.invokeGetMethod(api, bean, methodSuffix, parameters, throwException);
}
protected Object invokeGetMethod(ApiMethod apiMethod, Object bean,
String methodSuffix, Properties parameters, boolean throwException) throws ApiException, Throwable {
String methodName = null;
Object result = null;
try {
Class[] parameterTypes = new Class[]{Properties.class};
Class beanClass = bean.getClass();
methodName = (null != methodSuffix) ? apiMethod.getSpringBeanMethod() + methodSuffix.trim() : apiMethod.getSpringBeanMethod();
Method method = beanClass.getDeclaredMethod(methodName, parameterTypes);
result = method.invoke(bean, parameters);
} catch (NoSuchMethodException e) {
if (throwException) {
_logger.error("No such method '{}' of class '{}'", methodName, bean.getClass(), e);
throw new ApiException(IApiErrorCodes.API_METHOD_ERROR, "Method not supported - " + this.buildApiSignature(apiMethod), Response.Status.INTERNAL_SERVER_ERROR);
}
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof ApiException) {
throw (ApiException) e.getTargetException();
} else if (throwException) {
_logger.error("Error invoking method '{}' of class '{}'", methodName, bean.getClass());
throw new ApiException(IApiErrorCodes.API_METHOD_ERROR, "Error invoking Method - " + this.buildApiSignature(apiMethod), Response.Status.INTERNAL_SERVER_ERROR);
}
} catch (Throwable t) {
if (throwException) {
_logger.error("Error invoking method - {} {} of class '{}'", this.buildApiSignature(apiMethod) , methodName, bean.getClass(), t);
throw t;
}
}
return result;
}
protected Object invokePutPostDeleteMethod(ApiMethod apiMethod, Object bean,
Properties parameters, Object bodyObject) throws ApiException, Throwable {
Object result = null;
try {
if (apiMethod.getHttpMethod().equals(ApiMethod.HttpMethod.DELETE)) {
result = this.invokeDeleteMethod(apiMethod, bean, parameters);
} else {
result = this.invokePutPostMethod(apiMethod, bean, parameters, bodyObject);
}
if (null != result) return result;
StringApiResponse response = new StringApiResponse();
response.setResult(SUCCESS, null);
result = response;
} catch (NoSuchMethodException e) {
_logger.error("No such method '{}' of class '{}'",apiMethod.getSpringBeanMethod(), bean.getClass(), e);
throw new ApiException(IApiErrorCodes.API_METHOD_ERROR, "Method not supported - " + this.buildApiSignature(apiMethod), Response.Status.INTERNAL_SERVER_ERROR);
} catch (InvocationTargetException e) {
if (e.getTargetException() instanceof ApiException) {
throw (ApiException) e.getTargetException();
} else {
_logger.error("Error invoking method '{}' of class '{}'",apiMethod.getSpringBeanMethod(), bean.getClass());
throw new ApiException(IApiErrorCodes.API_METHOD_ERROR, "Error invoking Method - " + this.buildApiSignature(apiMethod), Response.Status.INTERNAL_SERVER_ERROR);
}
} catch (Throwable t) {
_logger.error("Error invoking method '{}' of class '{}'",apiMethod.getSpringBeanMethod(), bean.getClass(), t);
throw t;
}
return result;
}
private Object invokePutPostMethod(ApiMethod api, Object bean,
Properties parameters, Object bodyObject) throws NoSuchMethodException, InvocationTargetException, Throwable {
Object result = null;
Class beanClass = bean.getClass();
String methodName = api.getSpringBeanMethod();
try {
Class[] parameterTypes = new Class[]{bodyObject.getClass(), Properties.class};
Method method = beanClass.getDeclaredMethod(methodName, parameterTypes);
result = method.invoke(bean, bodyObject, parameters);
} catch (NoSuchMethodException e) {
//the first exception of type "NoSuchMethodException" will not catched... the second yes
Class[] parameterTypes = new Class[]{bodyObject.getClass()};
Method method = beanClass.getDeclaredMethod(methodName, parameterTypes);
result = method.invoke(bean, bodyObject);
} catch (InvocationTargetException e) {
throw e;
} catch (Throwable t) {
throw t;
}
return result;
}
private Object invokeDeleteMethod(ApiMethod api, Object bean,
Properties parameters) throws NoSuchMethodException, InvocationTargetException, Throwable {
Class[] parameterTypes = new Class[]{Properties.class};
Class beanClass = bean.getClass();
String methodName = api.getSpringBeanMethod();
Method method = beanClass.getDeclaredMethod(methodName, parameterTypes);
return method.invoke(bean, parameters);
}
protected IApiCatalogManager getApiCatalogManager() {
return _apiCatalogManager;
}
public void setApiCatalogManager(IApiCatalogManager apiCatalogManager) {
this._apiCatalogManager = apiCatalogManager;
}
protected IVelocityRenderer getVelocityRenderer() {
return _velocityRenderer;
}
public void setVelocityRenderer(IVelocityRenderer velocityRenderer) {
this._velocityRenderer = velocityRenderer;
}
protected BeanFactory getBeanFactory() {
return this._beanFactory;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this._beanFactory = beanFactory;
}
protected ServletContext getServletContext() {
return this._servletContext;
}
@Override
public void setServletContext(ServletContext servletContext) {
this._servletContext = servletContext;
}
private IApiCatalogManager _apiCatalogManager;
private IVelocityRenderer _velocityRenderer;
private BeanFactory _beanFactory;
private ServletContext _servletContext;
}