/*
* 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.cocoon.rest.controller;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cocoon.controller.Controller;
import org.apache.cocoon.pipeline.util.URLConnectionUtils;
import org.apache.cocoon.rest.controller.annotation.BaseURL;
import org.apache.cocoon.rest.controller.annotation.Inject;
import org.apache.cocoon.rest.controller.annotation.RESTController;
import org.apache.cocoon.rest.controller.annotation.RequestHeader;
import org.apache.cocoon.rest.controller.annotation.RequestParameter;
import org.apache.cocoon.rest.controller.annotation.SitemapParameter;
import org.apache.cocoon.rest.controller.response.Page;
import org.apache.cocoon.rest.controller.response.RestResponse;
import org.apache.cocoon.rest.controller.response.Status;
import org.apache.cocoon.rest.controller.util.AnnotationCollector;
import org.apache.cocoon.servlet.controller.ControllerContextHelper;
import org.apache.cocoon.servlet.node.StatusCodeCollector;
import org.apache.cocoon.servlet.util.HttpContextHelper;
import org.apache.cocoon.sitemap.util.ExceptionHandler;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* <p>
* This controller is responsible for the execution of instances of REST controller beans. Note that each controller
* implementation must be available as Spring bean. For that purpose you can use the {@link RESTController} annotation
* and load all beans from a particular package automatically. See
* http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-classpath-scanning for details.
* </p>
*/
public class SpringRESTController implements Controller, ApplicationContextAware {
private AnnotationCollector annotationCollector;
private ApplicationContext applicationContext;
private MethodDelegator methodDelegator;
public void invoke(OutputStream outputStream, String functionName, Map<String, Object> inputParameters,
Map<String, ? extends Object> configuration) {
if (!this.applicationContext.isPrototype(functionName)) {
throw new RuntimeException("A REST controller bean MUST run within the 'prototype' scope.");
}
try {
// get the prepared controller
Object controller = this.getController(functionName, inputParameters, configuration);
// invoke the appropriate method
HttpServletRequest request = HttpContextHelper.getRequest(inputParameters);
RestResponse restResponse = this.methodDelegator.delegate(request, controller);
// forward the response
if (restResponse instanceof Status) {
Status status = (Status) restResponse;
StatusCodeCollector.setStatusCode(status.getStatus());
} else if (restResponse instanceof Page) {
Page page = (Page) restResponse;
ControllerContextHelper.storeContext(page.getData(), inputParameters);
URL pageUri = new URL(new URL("servlet:"), page.getUri());
URLConnection servletConnection = pageUri.openConnection();
IOUtils.copy(servletConnection.getInputStream(), outputStream);
URLConnectionUtils.closeQuietly(servletConnection);
}
} catch (Exception e) {
throw ExceptionHandler.getInvocationException(e);
}
}
public void setAnnotationCollector(AnnotationCollector annotationCollector) {
this.annotationCollector = annotationCollector;
}
/**
* {@inheritDoc}
*
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void setMethodDelegator(MethodDelegator methodDelegator) {
this.methodDelegator = methodDelegator;
}
@SuppressWarnings("unchecked")
private Object getController(String controllerName, Map<String, Object> inputParameters,
Map<String, ? extends Object> configuration) throws Exception {
Object controller = this.applicationContext.getBean(controllerName);
Map<Class<? extends Annotation>, List<Field>> annotatedFields = this.annotationCollector
.getAnnotatedFields(controller.getClass());
// populate the annotated fields
populateInjectFields(inputParameters, controller, annotatedFields);
populateRequestFields(inputParameters, controller, annotatedFields);
populateRequestHeaderFields(inputParameters, controller, annotatedFields);
populateSitemapParameters(configuration, controller, annotatedFields);
this.populateBaseURL(configuration, controller, annotatedFields);
return controller;
}
private void populateBaseURL(Map<String, ? extends Object> configuration, Object controller,
Map<Class<? extends Annotation>, List<Field>> annotatedFields) throws IllegalAccessException, Exception {
List<Field> baseURLFields = annotatedFields.get(BaseURL.class);
if (baseURLFields == null || baseURLFields.isEmpty()) {
return;
}
for (Field field : baseURLFields) {
field.setAccessible(true);
Class<?> fieldType = field.getType();
if (fieldType == URL.class) {
field.set(controller, configuration.get("baseUrl"));
} else {
throw new Exception("The annotation " + BaseURL.class.getName() + " can only be set on fields of type "
+ URL.class.getName() + "." + " " + "(field=" + field.getName() + ", type="
+ fieldType.getName() + ")");
}
}
}
private static void populateInjectFields(Map<String, ? extends Object> parameters, Object controller,
Map<Class<? extends Annotation>, List<Field>> annotatedFields) throws IllegalAccessException, IOException,
Exception {
List<Field> injectFields = annotatedFields.get(Inject.class);
if (injectFields == null || injectFields.isEmpty()) {
return;
}
HttpServletRequest request = HttpContextHelper.getRequest(parameters);
HttpServletResponse response = HttpContextHelper.getResponse(parameters);
for (Field field : injectFields) {
field.setAccessible(true);
Class<?> fieldType = field.getType();
if (fieldType == HttpServletRequest.class) {
field.set(controller, request);
} else if (fieldType == HttpServletResponse.class) {
field.set(controller, response);
} else if (fieldType == Log.class) {
field.set(controller, LogFactory.getLog(controller.getClass()));
} else if (fieldType == ServletInputStream.class || fieldType == InputStream.class) {
field.set(controller, request.getInputStream());
} else if (fieldType == ServletOutputStream.class || fieldType == OutputStream.class) {
field.set(controller, response.getOutputStream());
} else {
throw new Exception("The annotation " + Inject.class.getName()
+ " doesn't support the injection of type " + fieldType.getName() + "." + " " + "(field="
+ field.getName() + ", type=" + fieldType.getName() + ")");
}
}
}
private static void populateRequestFields(Map<String, ? extends Object> parameters, Object controller,
Map<Class<? extends Annotation>, List<Field>> annotatedFields) throws IllegalAccessException, Exception {
List<Field> requestFields = annotatedFields.get(RequestParameter.class);
if (requestFields == null || requestFields.isEmpty()) {
return;
}
HttpServletRequest request = HttpContextHelper.getRequest(parameters);
for (Field field : requestFields) {
field.setAccessible(true);
String requestParameterName = field.getAnnotation(RequestParameter.class).value();
if (isBlank(requestParameterName)) {
requestParameterName = field.getName();
}
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
String parameter = request.getParameter(requestParameterName);
if (parameter != null) {
field.set(controller, parameter);
}
} else if (fieldType == int.class) {
String parameter = request.getParameter(requestParameterName);
if (parameter != null) {
field.set(controller, Integer.parseInt(parameter));
}
} else if (fieldType == boolean.class) {
String parameter = request.getParameter(requestParameterName);
if (parameter != null) {
field.set(controller, Boolean.parseBoolean(parameter));
}
} else if (fieldType == String[].class) {
String[] parameterValues = request.getParameterValues(requestParameterName);
if (parameterValues != null) {
field.set(controller, parameterValues);
}
} else {
throw new Exception("The annotation " + RequestParameter.class.getName()
+ " can only be set on fields of type " + String.class.getName() + ", "
+ String[].class.getName() + ", " + int.class.getName() + " or " + boolean.class.getName()
+ ". (field=" + field.getName() + ", type=" + fieldType.getName() + ")");
}
}
}
private static void populateRequestHeaderFields(Map<String, ? extends Object> parameters, Object controller,
Map<Class<? extends Annotation>, List<Field>> annotatedFields) throws IllegalAccessException, Exception {
List<Field> requestHeaderFields = annotatedFields.get(RequestHeader.class);
if (requestHeaderFields == null || requestHeaderFields.isEmpty()) {
return;
}
HttpServletRequest request = HttpContextHelper.getRequest(parameters);
for (Field field : requestHeaderFields) {
field.setAccessible(true);
String name = field.getAnnotation(RequestHeader.class).value();
if (isBlank(name)) {
name = field.getName();
}
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
String header = request.getHeader(name);
if (header != null) {
field.set(controller, header);
}
} else {
throw new Exception("The annotation " + RequestHeader.class.getName()
+ " can only be set on fields of type " + String.class.getName() + "." + " " + "(field="
+ field.getName() + ", type=" + fieldType.getName() + ")");
}
}
}
private static void populateSitemapParameters(Map<String, ? extends Object> configuration, Object controller,
Map<Class<? extends Annotation>, List<Field>> annotatedFields) throws IllegalAccessException, Exception {
List<Field> sitemapParameterFields = annotatedFields.get(SitemapParameter.class);
if (sitemapParameterFields == null || sitemapParameterFields.isEmpty()) {
return;
}
for (Field field : sitemapParameterFields) {
field.setAccessible(true);
String name = field.getAnnotation(SitemapParameter.class).value();
if (isBlank(name)) {
name = field.getName();
}
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
field.set(controller, configuration.get(name));
} else {
throw new Exception("The annotation " + SitemapParameter.class.getName()
+ " can only be set on fields of type " + String.class.getName() + "." + " " + "(field="
+ field.getName() + ", type=" + fieldType.getName() + ")");
}
}
}
private static boolean isBlank(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (Character.isWhitespace(str.charAt(i)) == false) {
return false;
}
}
return true;
}
}