Skip to content

Commit

Permalink
Feature service core almost rewrote
Browse files Browse the repository at this point in the history
  • Loading branch information
aaime committed Jun 23, 2019
1 parent b83b7f1 commit 2b9d9a6
Show file tree
Hide file tree
Showing 15 changed files with 1,140 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,26 @@
package org.geoserver.api;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import org.geoserver.config.GeoServer;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.DispatcherCallback;
import org.geoserver.ows.Request;
import org.geoserver.ows.Response;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.Operation;
import org.geoserver.platform.ServiceException;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServletServerHttpRequest;
Expand All @@ -49,26 +57,30 @@ public class APIBodyMethodProcessor extends RequestResponseBodyMethodProcessor {
private final ContentNegotiationManager contentNegotiationManager;
protected final GeoServerResourceLoader loader;
protected final GeoServer geoServer;
protected List<DispatcherCallback> callbacks;

public APIBodyMethodProcessor(
List<HttpMessageConverter<?>> converters,
GeoServerResourceLoader loader,
GeoServer geoServer) {
this(converters, new APIContentNegotiationManager(), loader, geoServer);
GeoServer geoServer,
List<DispatcherCallback> callbacks) {
this(converters, new APIContentNegotiationManager(), loader, geoServer, callbacks);
}

public APIBodyMethodProcessor(
List<HttpMessageConverter<?>> converters,
ContentNegotiationManager contentNegotiationManager,
GeoServerResourceLoader loader,
GeoServer geoServer) {
GeoServer geoServer,
List<DispatcherCallback> callbacks) {
super(
converters,
new APIContentNegotiationManager(), // this is the customized bit
Collections.singletonList(new JsonViewResponseBodyAdvice()));
this.contentNegotiationManager = contentNegotiationManager;
this.loader = loader;
this.geoServer = geoServer;
this.callbacks = callbacks;
}

protected <T> void writeWithMessageConverters(
Expand All @@ -80,19 +92,42 @@ protected <T> void writeWithMessageConverters(
HttpMessageNotWritableException {
HTMLResponseBody htmlResponseBody = returnType.getMethodAnnotation(HTMLResponseBody.class);
MediaType mediaType = getMediaTypeToUse(value, returnType, inputMessage, outputMessage);
HttpMessageConverter converter;
if (htmlResponseBody != null && MediaType.TEXT_HTML.isCompatibleWith(mediaType)) {
// direct HTML encoding based on annotations
SimpleHTTPMessageConverter converter =
converter =
new SimpleHTTPMessageConverter(
value.getClass(),
returnType.getContainingClass(),
loader,
geoServer,
htmlResponseBody.templateName());
converter.write(value, MediaType.TEXT_HTML, outputMessage);
mediaType = MediaType.TEXT_HTML;
} else {
super.writeWithMessageConverters(value, returnType, inputMessage, outputMessage);
converter = getMessageConverter(value, returnType, inputMessage, outputMessage);
}

// DispatcherCallback bridging
final MediaType finalMediaType = mediaType;
Response response = new Response(value.getClass()) {

@Override
public String getMimeType(Object value, Operation operation) throws ServiceException {
return finalMediaType.toString();
}

@Override
public void write(Object value, OutputStream output, Operation operation) throws IOException, ServiceException {
converter.write(value, finalMediaType, outputMessage);
}
};

Request dr = Dispatcher.REQUEST.get();
response = fireResponseDispatchedCallback(dr, dr.getOperation(), value, response);

// write using the response provided by the callbacks
outputMessage.getHeaders().setContentType(MediaType.parseMediaType(response.getMimeType(value, dr.getOperation())));
response.write(value, outputMessage.getServletResponse().getOutputStream(), dr.getOperation());
}

private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
Expand Down Expand Up @@ -213,4 +248,52 @@ private Type getGenericType(MethodParameter returnType) {
return returnType.getGenericParameterType();
}
}

protected <T> HttpMessageConverter getMessageConverter(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class valueType;
Type targetType;
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
} else {
body = value;
valueType = this.getReturnValueType(value, returnType);
targetType = GenericTypeResolver.resolveType(this.getGenericType(returnType), returnType.getContainingClass());
}

MediaType selectedMediaType = getMediaTypeToUse(value, returnType, inputMessage, outputMessage);

if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
return converter;
}
}
}

if (body != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
return null;
}

Response fireResponseDispatchedCallback(
Request req, Operation op, Object result, Response response) {
for (DispatcherCallback cb : callbacks) {
Response r = cb.responseDispatched(req, op, result, response);
response = r != null ? r : response;
}
return response;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ protected Object doInvoke(Object... args) throws Exception {
getBridgedMethod(),
args);
operation = fireOperationDispatchedCallback(request, operation);
request.setOperation(operation);
try {
return operation
.getMethod()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.geoserver.api.features.RFCGeoJSONFeaturesResponse;
import org.springframework.http.MediaType;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
Expand Down Expand Up @@ -62,7 +64,7 @@ private static class JSONContentNegotiationStrategy implements ContentNegotiatio
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
// default to JSON, allow all
return Arrays.asList(MediaType.APPLICATION_JSON, MediaType.ALL);
return Arrays.asList(MediaType.parseMediaType(RFCGeoJSONFeaturesResponse.MIME), MediaType.APPLICATION_JSON, MediaType.ALL);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@
import org.geoserver.ows.Request;
import org.geoserver.ows.util.KvpMap;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geoserver.platform.Service;
import org.geoserver.platform.ServiceException;
import org.geoserver.platform.*;
import org.geotools.util.Version;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
Expand Down Expand Up @@ -120,9 +117,11 @@ protected boolean isHandler(Class<?> beanType) {
this.messageConverters = handlerAdapter.getMessageConverters();

// add custom argument resolvers
List<HandlerMethodArgumentResolver> argumentResolves =
List<HandlerMethodArgumentResolver> pluginResolvers =
GeoServerExtensions.extensions(HandlerMethodArgumentResolver.class);
addToListBackwards(argumentResolves, handlerAdapter.getArgumentResolvers());
List<HandlerMethodArgumentResolver> adapterResolvers = new ArrayList<>(handlerAdapter.getArgumentResolvers());
addToListBackwards(pluginResolvers, adapterResolvers);
handlerAdapter.setArgumentResolvers(adapterResolvers);

// default treatment of "f" parameter and headers, defaulting to JSON if nothing else has
// been provided
Expand All @@ -140,7 +139,8 @@ protected boolean isHandler(Class<?> beanType) {
handlerAdapter.getMessageConverters(),
GeoServerExtensions.bean(
GeoServerResourceLoader.class),
GeoServerExtensions.bean(GeoServer.class));
GeoServerExtensions.bean(GeoServer.class),
callbacks);
} else {
return f;
}
Expand Down Expand Up @@ -213,6 +213,7 @@ protected ModelAndView handleRequestInternal(
// set request / response
dr.setHttpRequest(httpRequest);
dr.setHttpResponse(httpResponse);
dr.setGet("GET".equalsIgnoreCase(httpRequest.getMethod()));

try {
// initialize the request and allow callbacks to override it
Expand Down Expand Up @@ -241,6 +242,8 @@ protected ModelAndView handleRequestInternal(

// and this is response handling
Object returnValue = mav.getModel().get(RESPONSE_OBJECT);
returnValue = fireOperationExecutedCallback(dr, dr.getOperation(), returnValue);

returnValueHandlers.handleReturnValue(
returnValue,
new ReturnValueMethodParameter(handler.getMethod(), returnValue),
Expand Down Expand Up @@ -451,6 +454,16 @@ Service fireServiceDispatchedCallback(Request req, Service service) {
return service;
}

// SHARE
Object fireOperationExecutedCallback(Request req, Operation op, Object result) {
for (DispatcherCallback cb : callbacks) {
Object r = cb.operationExecuted(req, op, result);
result = r != null ? r : result;
}
return result;
}


/**
* This comes from {@link org.springframework.web.servlet.DispatcherServlet}, it's private and
* thus not reusable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.geoserver.api;

import org.geoserver.api.features.FeaturesResponse;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;
import org.geoserver.ows.Response;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.Operation;
import org.geoserver.wfs.WFSGetFeatureOutputFormat;
import org.geoserver.wfs.request.FeatureCollectionResponse;
import org.geotools.util.Version;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Base class for adapting {@link Response} objects for a given response type and HttpMessageConverter interface
* @param <T>
*/
public class MessageConverterResponseAdapter<T> implements HttpMessageConverter<T>, ApplicationContextAware {


Class<T> valueClass;
Class responseBinding;
List<Response> responses;
private List<MediaType> supportedMediaTypes;

public MessageConverterResponseAdapter(Class<T> valueClass, Class responseBinding) {
this.valueClass = valueClass;
this.responseBinding = responseBinding;
}

@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// write only
return false;
}

@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return valueClass.isAssignableFrom(aClass) && (mediaType == null || getResponse(mediaType) != null);
}

@Override
public List<MediaType> getSupportedMediaTypes() {
return supportedMediaTypes;
}

@Override
public T read(Class<? extends T> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
throw new UnsupportedOperationException();
}

@Override
public void write(T value, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
Optional<Response> response = getResponse(mediaType);
if (!response.isPresent()) {
throw new IllegalArgumentException("Could not find a Response handling " + mediaType + " for binding " + valueClass);
}

Request dr = Dispatcher.REQUEST.get();
Operation operation = getOperation(value, dr);
writeResponse(value, httpOutputMessage, operation, response.get());
}

protected void writeResponse(T value, HttpOutputMessage httpOutputMessage, Operation operation, Response response) throws IOException {
response.write(value, httpOutputMessage.getBody(), operation);
}

protected Operation getOperation(T featuresResponse, Request dr) {
return dr.getOperation();
}

public Optional<Response> getResponse(MediaType mediaType) {
return responses.stream().filter(r -> getMediaTypeStream(r).anyMatch(mt -> mediaType.isCompatibleWith(mt))).findFirst();
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.responses = GeoServerExtensions.extensions(Response.class, applicationContext).stream().filter(getResponseFilterPredicate()).collect(Collectors.toList());
this.supportedMediaTypes = this.responses.stream()
.flatMap(r -> getMediaTypeStream(r))
.distinct()
.collect(Collectors.toList());
}

protected Predicate<Response> getResponseFilterPredicate() {
return r -> responseBinding.isAssignableFrom(r.getBinding());
}

private Stream<MediaType> getMediaTypeStream(Response r) {
return r.getOutputFormats().stream().filter(f -> f.contains("/")).filter(f -> {
// GML2 content type is not really valid, this is here to filter rough content types
try {
MediaType.parseMediaType(f);
return true;
} catch (Exception e) {
return false;
}
}).map(f -> MediaType.parseMediaType(f));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
*
*/

/*
* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* * This code is licensed under the GPL 2.0 license, available at the root
* * application directory.
*
*/

package org.geoserver.api.features;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
Expand Down

0 comments on commit 2b9d9a6

Please sign in to comment.