I am new to Camel and I am facing an issue while sending files to webservice via camel http.
I have a rest web service which consumes Multipart form data type content and accepts input as part of form data.
When I send file and form parameter via camel it gives me the following error at camel console:
Stacktrace
---------------------------------------------------------------------------------------------------------------------------------------
org.apache.camel.component.http.HttpOperationFailedException: HTTP operation failed invoking http://localhost:8080/JAX_RS_Application/resource/restwb/upload with statusCode: 415
at org.apache.camel.component.http.HttpProducer.populateHttpOperationFailedException(HttpProducer.java:230)
at org.apache.camel.component.http.HttpProducer.process(HttpProducer.java:156)
at org.apache.camel.util.AsyncProcessorConverterHelper$ProcessorToAsyncProcessorBridge.process(AsyncProcessorConverterHelper.java:61)
at org.apache.camel.processor.SendProcessor.process(SendProcessor.java:129)
at org.apache.camel.management.InstrumentationProcessor.process(InstrumentationProcessor.java:77)
at org.apache.camel.processor.RedeliveryErrorHandler.process(RedeliveryErrorHandler.java:448)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:191)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:118)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:80)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:191)
at org.apache.camel.component.file.GenericFileConsumer.processExchange(GenericFileConsumer.java:435)
at org.apache.camel.component.file.GenericFileConsumer.processBatch(GenericFileConsumer.java:211)
at org.apache.camel.component.file.GenericFileConsumer.poll(GenericFileConsumer.java:175)
at org.apache.camel.impl.ScheduledPollConsumer.doRun(ScheduledPollConsumer.java:174)
at org.apache.camel.impl.ScheduledPollConsumer.run(ScheduledPollConsumer.java:101)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:304)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:178)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)
The error i get on the server side console is as follows:
SEVERE: MessageBodyReader not found for media type=application/octet-stream, typ
e=class org.glassfish.jersey.media.multipart.FormDataMultiPart, genericType=clas
s org.glassfish.jersey.media.multipart.FormDataMultiPart.
The code snippet of the Rest web-service created via jersey is as follows:
import java.io.IOException;
import java.io.InputStream;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
#Path("/restwb")
public class FileResource {
#POST
#Path("/upload")
#Consumes(MediaType.MULTIPART_FORM_DATA)
public String uploadFile(#FormDataParam("username") String username,#FormDataParam("password") String password,#FormDataParam("upload") InputStream is) {
String output ="Hi "+username+" your password is "+password;
output=output+IOUtils.LINE_SEPARATOR +IOUtils.LINE_SEPARATOR;
output=output+"Output :"+IOUtils.LINE_SEPARATOR+"------------------------------------------------------------------------------"+IOUtils.LINE_SEPARATOR;
try {
output=output+IOUtils.toString(is)+IOUtils.LINE_SEPARATOR+IOUtils.LINE_SEPARATOR;
output=output+"==================================================================================================="+IOUtils.LINE_SEPARATOR+IOUtils.LINE_SEPARATOR;
System.out.println("Output :"+output);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return output;
}
}
And my Camel config is as follows:
import org.apache.camel.*;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spi.Synchronization;
import org.apache.camel.spi.UnitOfWork;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.james.mime4j.message.Multipart;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* Created by Manish.Pillai on 7/16/2015.
*/
public class LoggingMain {
private static final Logger logger =Logger.getLogger(LoggingMain.class);
public static void main(String[] args) throws Exception{
CamelContext camelContext =new DefaultCamelContext();
try {
camelContext.addRoutes(new RouteBuilder() {
#Override
public void configure() throws Exception {
from("file:C:\\temp?delay=5000&move=processed&moveFailed=error&antExclude=**/processed/**,**/error/**")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
exchange.getContext().getTypeConverterRegistry().addTypeConverter(HttpEntity.class,InputStream.class,new InputStreamToHttpEntityConvertor());
exchange.getOut().setBody(exchange.getIn().getBody(),HttpEntity.class);
}
})
.to("http://localhost:8080/JAX_RS_Application/resource/restwb/upload");
}
});
camelContext.getRestConfiguration();
camelContext.start();
Thread.sleep(5000);
camelContext.stop();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
static class InputStreamToHttpEntityConvertor implements TypeConverter {
public boolean allowNull() {
return false;
}
public <T> T convertTo(Class<T> type, Object value) throws TypeConversionException {
Exchange exchange=(Exchange)value;
StringBody username = new StringBody("username", ContentType.MULTIPART_FORM_DATA);
StringBody password = new StringBody("password", ContentType.MULTIPART_FORM_DATA);
MultipartEntityBuilder multipartEntityBuilder=MultipartEntityBuilder.create();
multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
multipartEntityBuilder.addPart("upload", new FileBody(exchange.getIn().getBody(File.class), ContentType.MULTIPART_FORM_DATA, (String) exchange.getIn().getHeader(Exchange.FILE_NAME)));
multipartEntityBuilder.addPart("username",username);
multipartEntityBuilder.addPart("password",password);
return (T)multipartEntityBuilder.build();
}
public <T> T convertTo(Class<T> aClass, Exchange exchange, Object o) throws TypeConversionException {
return convertTo(aClass,o);
}
public <T> T mandatoryConvertTo(Class<T> type, Object value) throws TypeConversionException, NoTypeConversionAvailableException {
return convertTo(type,value);
}
public <T> T mandatoryConvertTo(Class<T> type, Exchange exchange, Object value) throws TypeConversionException, NoTypeConversionAvailableException {
return convertTo(type,value);
}
public <T> T tryConvertTo(Class<T> type, Object value) {
return convertTo(type,value);
}
public <T> T tryConvertTo(Class<T> type, Exchange exchange, Object value) {
return convertTo(type,value);
}
}
}
Any leads would be helpful.
Well, there are several things that can be improved in your code.
First, since you are using a MultipartEntityBuilder, that means you're using Apache's HttpClient version 4.3+, so for best compatibility you should use Camel's HTTP4 component.
Third, in an example as small as this, you don't really need to use the converter, you can do something like this:
public class LoggingMain {
private static final Logger logger = Logger.getLogger(LoggingMain.class);
public static void main(String[] args) throws Exception {
CamelContext camelContext = new DefaultCamelContext();
try {
camelContext.addRoutes(new RouteBuilder() {
#Override
public void configure() throws Exception {
from("file:C:\\temp?delay=5000&move=processed&moveFailed=error&antExclude=**/processed/**,**/error/**")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
StringBody username = new StringBody("username", ContentType.MULTIPART_FORM_DATA);
StringBody password = new StringBody("password", ContentType.MULTIPART_FORM_DATA);
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
multipartEntityBuilder.addPart("username", username);
multipartEntityBuilder.addPart("password", password);
String filename = (String) exchange.getIn().getHeader(Exchange.FILE_NAME);
File file = exchange.getIn().getBody(File.class);
multipartEntityBuilder.addPart("upload", new FileBody(file, ContentType.MULTIPART_FORM_DATA, filename));
exchange.getIn().setBody(multipartEntityBuilder.build());
}
})
.to("http4://localhost:8080/JAX_RS_Application/resource/restwb/upload");
}
});
camelContext.getRestConfiguration();
camelContext.start();
Thread.sleep(5000);
camelContext.stop();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}
I hope this helps!
Related
Is there a way to add a filter/interceptor to static resources served from META-INF/resources?
I seem have tried all the possible options: #ServerResponseFilter, ContainerResponseFilter,WriterInterceptor however all these functions are called only for #Path or #Route...
Is there anything similar to #RouteFilter but for response?
Those are the only ones available:
public interface ClientRequestFilter {
void filter(ClientRequestContext requestContext) throws IOException;
}
public interface ClientResponseFilter {
void filter(ClientRequestContext requestContext,
ClientResponseContext responseContext) throws IOException;
}
public interface ContainerRequestFilter {
void filter(ContainerRequestContext requestContext) throws IOException;
}
public interface ContainerResponseFilter {
void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException;
}
public interface ReaderInterceptor {
Object aroundReadFrom(ReaderInterceptorContext context)
throws java.io.IOException, javax.ws.rs.WebApplicationException;
}
public interface WriterInterceptor {
void aroundWriteTo(WriterInterceptorContext context)
throws java.io.IOException, javax.ws.rs.WebApplicationException;
}
You could try with ClientResponseFilter and see:
In the Client API, a ClientRequestFilter is executed as part of the
invocation pipeline, before the HTTP request is delivered to the
network.
ClientResponseFilter is executed upon receiving a server response,
before control is returned to the application.
https://quarkus.io/specs/jaxrs/2.1/index.html#filters_and_interceptors
There seem to be nothing built-in to modify the response of a static content for now but nothing stops you from serving content in a dynamic way:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
#Path("/")
#Produces(MediaType.TEXT_HTML)
public class FrontendController {
#GET
public Response index() throws IOException {
try (var index = getClass().getResourceAsStream(FrontendConstants.INDEX_RESOURCE)) {
if (index == null) {
throw new IOException("index.html file does not exist");
}
//modify index
return Response
.ok(index)
.cacheControl(FrontendConstants.NO_CACHE)
.build();
}
}
#GET
#Path("/{fileName:.+}")
public Response staticFile(#PathParam("fileName") String fileName) throws IOException {
try (var file = FrontendController.class.getResourceAsStream(FrontendConstants.FRONTEND_DIR + "/" + fileName)) {
if (file == null) {
return index();
}
var contentType = URLConnection.guessContentTypeFromStream(file);
return Response
.ok(
new String(file.readAllBytes(), StandardCharsets.UTF_8),
contentType == null ? MediaType.TEXT_PLAIN : contentType
)
.cacheControl(FrontendConstants.NO_CACHE)
.build();
}
}
}
I need to make my Spring Boot application start/stop listening on a new port dynamically.
I understand a new tomcat connector needs to be injected into Spring context for this.
I'm able to add a connector using a ServletWebServerFactory bean and tomcatConnectorCustomizer. But this bean is loaded only during Spring Bootup.
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
connector.setPort(serverPort);
connector.setScheme("https");
connector.setSecure(true);
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
protocol.setSSLEnabled(true);
protocol.setKeystoreType("PKCS12");
protocol.setKeystoreFile(keystorePath);
protocol.setKeystorePass(keystorePass);
protocol.setKeyAlias("spa");
protocol.setSSLVerifyClient(Boolean.toString(true));
tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
return tomcat;
}
}
Is there any way to add a tomcat connector during run time? Say on a method call?
Update: Connector created but all requests to it return 404:
I've managed to add a Tomcat connector at runtime. But the request made to that port are not going to my RestController.
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
connector.setScheme("http");
connector.setSecure(false);
connector.setPort(8472);
protocol.setSSLEnabled(false);
};
tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
tomcat.getWebServer().start();
How should I proceed further?
Hi here is my sample project: sample project
1- Main Application (DemoApplication.java):
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2 - Config file (AppConfig.java):
#Configuration
public class AppConfig {
#Autowired
private ServletWebServerApplicationContext server;
private static FilterConfig filterConfig = new FilterConfig();
#PostConstruct
void init() {
//setting default port config
filterConfig.addNewPortConfig(8080, "/admin");
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public FilterConfig createFilterConfig() {
return filterConfig;
}
public void addPort(String schema, String domain, int port, boolean secure) {
TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
synchronized (this) {
ts.getTomcat().setConnector(createConnector(schema, domain, port, secure));
}
}
public void addContextAllowed(FilterConfig filterConfig, int port, String context) {
filterConfig.addNewPortConfig(port, context);
}
public void removePort(int port) {
TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
Service service = ts.getTomcat().getService();
synchronized (this) {
Connector[] findConnectors = service.findConnectors();
for (Connector connector : findConnectors) {
if (connector.getPort() == port) {
try {
connector.stop();
connector.destroy();
filterConfig.removePortConfig(port);
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
}
}
private Connector createConnector(String schema, String domain, int port, boolean secure) {
Connector conn = new Connector("org.apache.coyote.http11.Http11NioProtocol");
conn.setScheme(schema);
conn.setPort(port);
conn.setSecure(true);
conn.setDomain(domain);
if (secure) {
// config secure port...
}
return conn;
}
}
3 - Filter (NewPortFilter.java):
public class NewPortFilter {
#Bean(name = "restrictFilter")
public FilterRegistrationBean<Filter> retstrictFilter(FilterConfig filterConfig) {
Filter filter = new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// get allowed url contexts
Set<String> config = filterConfig.getConfig().get(request.getLocalPort());
if (config == null || config.isEmpty()) {
response.sendError(403);
}
boolean accepted = false;
for (String value : config) {
if (request.getPathInfo().startsWith(value)) {
accepted = true;
break;
}
}
if (accepted) {
filterChain.doFilter(request, response);
} else {
response.sendError(403);
}
}
};
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setOrder(-100);
filterRegistrationBean.setName("restrictFilter");
return filterRegistrationBean;
}
}
4 - Filter Config (FilterConfig.java):
public class FilterConfig {
private Map<Integer, Set<String>> acceptedContextsByPort = new ConcurrentHashMap<>();
public void addNewPortConfig(int port, String allowedContextUrl) {
if(port > 0 && allowedContextUrl != null) {
Set<String> set = acceptedContextsByPort.get(port);
if (set == null) {
set = new HashSet<>();
}
set = new HashSet<>(set);
set.add(allowedContextUrl);
acceptedContextsByPort.put(port, set);
}
}
public void removePortConfig(int port) {
if(port > 0) {
acceptedContextsByPort.remove(port);
}
}
public Map<Integer, Set<String>> getConfig(){
return acceptedContextsByPort;
}
}
5 - Controller (TestController.java):
#RestController
public class TestController {
#Autowired
AppConfig config;
#Autowired
FilterConfig filterConfig;
#GetMapping("/admin/hello")
String test() {
return "hello test";
}
#GetMapping("/alternative/hello")
String test2() {
return "hello test 2";
}
#GetMapping("/admin/addNewPort")
ResponseEntity<String> createNewPort(#RequestParam Integer port, #RequestParam String context) {
if (port == null || port < 1) {
return new ResponseEntity<>("Invalid Port" + port, HttpStatus.BAD_REQUEST);
}
config.addPort("http", "localhost", port, false);
if (context != null && context.length() > 0) {
config.addContextAllowed(filterConfig, port, context);
}
return new ResponseEntity<>("Added port:" + port, HttpStatus.OK);
}
#GetMapping("/admin/removePort")
ResponseEntity<String> removePort(#RequestParam Integer port) {
if (port == null || port < 1) {
return new ResponseEntity<>("Invalid Port" + port, HttpStatus.BAD_REQUEST);
}
config.removePort(port);
return new ResponseEntity<>("Removed port:" + port, HttpStatus.OK);
}
}
How to test it?
In a browser:
1 - try:
http://localhost:8080/admin/hello
Expected Response: hello test
2 - try:
http://localhost:8080/admin/addNewPort?port=9090&context=alternative
Expected Response: Added port:9090
3 - try:
http://localhost:9090/alternative/hello
Expected Response: hello test 2
4 - try expected errors:
http://localhost:9090/alternative/addNewPort?port=8181&context=alternative
Expected Response (context [alternative] allowed but endpoint not registered in controller for this context): Whitelabel Error Page...
http://localhost:9090/any/hello
Expected Response (context [any] not allowed): Whitelabel Error Page...
http://localhost:8888/any/hello
Expected Response (invalid port number): ERR_CONNECTION_REFUSED
http://localhost:8080/hello
Expected Response (no context allowed [/hello]): Whitelabel Error Page...
5 - try remove a port:
http://localhost:8080/admin/removePort?port=9090
6 - check removed port:
http://localhost:9090/alternative/hello
Expected Response (port closed): ERR_CONNECTION_REFUSED
I hope it helps.
You should create ServletWebServerFactory bean as a prototype bean using #Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE).
Now in bean where you need new tomcat connector to be injected into Spring context(MySingletonBean in example) autowire the application context and get ServletWebServerFactory bean(MyPrototypeBean in example) from getBean method. In this way you will always get new tomcat connector bean.
Following is a simple sample code:-
public class MySingletonBean {
#Autowired
private ApplicationContext applicationContext;
public void showMessage(){
MyPrototypeBean bean = applicationContext.getBean(MyPrototypeBean.class);
}
}
Based on the accepted answer of #ariel-carrera, I did it a little differently (IMO cleaner):
Define a normal Spring controller for handling the request on any dynamic port
import package.IgnoredBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.lang3.reflect.MethodUtils.getMethodsWithAnnotation;
//must be declared in a separate java file so that it's not picked up by component scanning as inner class
#IgnoredBean
#RestController
#Slf4j
#RequiredArgsConstructor
class DynamicController {
static final Method HANDLER_METHOD = getMethodsWithAnnotation(DynamicController.class, RequestMapping.class)[0];
private final String myContext;
#RequestMapping
public Object handle(
#RequestBody Map<String, Object> body,
#RequestParam MultiValueMap<String, Object> requestParams,
#PathVariable Map<String, Object> pathVariables
) {
Map<String, Object> allAttributes = new HashMap<>(body.size() + requestParams.size() + pathVariables.size());
allAttributes.putAll(body);
allAttributes.putAll(pathVariables);
requestParams.forEach((name, values) -> allAttributes.put(name, values.size() > 1 ? values : values.get(0)));
log.info("Handling request for '{}': {}", myContext, allAttributes);
return allAttributes;
}
// this handler only affects this particular controller. Otherwise it will use any of your regular #ControllerAdvice beans or fall back to spring's default
#ExceptionHandler
public ResponseEntity<?> onError(Exception e) {
log.debug("something happened in '{}'", myContext, e);
return ResponseEntity.status(500).body(Map.of("message", e.getMessage()));
}
}
Must be declared in its own file
It is not a Spring Bean, we will instantiate it manually for each port and give it some context objects that are relevant to the port owner.
Note the #IgnoredBean, a custom annotation:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target(TYPE)
#Retention(RUNTIME)
public #interface IgnoredBean {
}
#SpringBootApplication
#ComponentScan(excludeFilters = #ComponentScan.Filter(IgnoredBean.class))
...
public class MyApplication{...}
The rest of it
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toUnmodifiableSet;
#Service
#RequiredArgsConstructor
#Slf4j
class DynamicControllerService {
private final RequestMappingHandlerMapping requestHandlerMapper;
private final Map<Integer, RequestMappingInfo> mappingByPort = new ConcurrentHashMap<>();
private Tomcat tomcat;
#Autowired
void setTomcat(ServletWebServerApplicationContext context) {
tomcat = ((TomcatWebServer) context.getWebServer()).getTomcat();
}
public int addMapping(#Nullable Integer givenPort, RequestMethod method, String path, Object myContext) {
val connector = new Connector(new Http11NioProtocol());
connector.setThrowOnFailure(true);
//0 means it will pick any available port
connector.setPort(Optional.ofNullable(givenPort).orElse(0));
try {
tomcat.setConnector(connector);
} catch (IllegalArgumentException e) {
// if it fails to start the connector, the object will still be left inside here
tomcat.getService().removeConnector(connector);
val rootCause = ExceptionUtils.getRootCause(e);
throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
}
int port = connector.getLocalPort();
val mapping = RequestMappingInfo
.paths(path)
.methods(method)
.customCondition(new PortRequestCondition(port))
.build();
requestHandlerMapper.registerMapping(
mapping,
new DynamicController("my context for port " + port),
DynamicController.HANDLER_METHOD
);
mappingByPort.put(port, mapping);
log.info("added mapping {} {} for port {}", method, path, port);
return port;
}
public void removeMapping(Integer port) {
Stream.of(tomcat.getService().findConnectors())
.filter(connector -> connector.getPort() == port)
.findFirst()
.ifPresent(connector -> {
try {
tomcat.getService().removeConnector(connector);
connector.destroy();
} catch (IllegalArgumentException | LifecycleException e) {
val rootCause = ExceptionUtils.getRootCause(e);
throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
}
val mapping = mappingByPort.get(port);
requestHandlerMapper.unregisterMapping(mapping);
log.info("removed mapping {} {} for port {}",
mapping.getMethodsCondition().getMethods(),
Optional.ofNullable(mapping.getPathPatternsCondition())
.map(PathPatternsRequestCondition::getPatternValues)
.orElse(Set.of()),
port
);
});
}
#RequiredArgsConstructor
private static class PortRequestCondition implements RequestCondition<PortRequestCondition> {
private final Set<Integer> ports;
public PortRequestCondition(Integer... ports) {
this.ports = Set.of(ports);
}
#Override
public PortRequestCondition combine(PortRequestCondition other) {
return new PortRequestCondition(Stream.concat(ports.stream(), other.ports.stream()).collect(toUnmodifiableSet()));
}
#Override
public PortRequestCondition getMatchingCondition(HttpServletRequest request) {
return ports.contains(request.getLocalPort()) ? this : null;
}
#Override
public int compareTo(PortRequestCondition other, HttpServletRequest request) {
return 0;
}
}
}
I'm trying to work out a simple web app with angular as the front end and Spring Boot as the back end. My pre-flight request succeeds and I receive a valid token, however when trying to make a subsequent request to a route I get a 404. When I try to curl the url I get a 500 with the message "Missing or invalid authorization header" which seems to me to be saying that the route does indeed exist and is listening, but something else is wrong.
First of all the Typescript. Here is my login.component.ts
import { Component } from "#angular/core";
import { Observable } from "rxjs/index";
import { LoginService } from "../services/login.service";
#Component({
selector: "login",
templateUrl: "./login.component.html"
})
export class Login {
private model = {"username": "", "password": ""};
private currentUserName;
constructor (private loginService: LoginService) {
this.currentUserName = localStorage.getItem("currentUserName");
}
public onSubmit() {
console.log("submitting");
this.loginService.sendCredential(this.model).subscribe(
data => {
console.log(data);
localStorage.setItem("token", data);
console.log("Setting token");
this.loginService.sendToken(localStorage.getItem("token")).subscribe(
data => {
this.currentUserName = this.model.username;
localStorage.setItem("currentUserName", this.model.username);
this.model.username = "";
this.model.password = "";
},
error => console.log(error)
);
},
error => {
console.log("oh no");
console.log(error)
}
);
}
}
And then my LoginService
import { Injectable } from "#angular/core";
import { HttpClient } from '#angular/common/http';
import { HttpHeaders } from '#angular/common/http';
import { Observable } from "rxjs/index";
#Injectable()
export class LoginService {
token: string;
constructor (private http: HttpClient) {}
public sendCredential(model): Observable<String> {
const tokenUrlPreFlight = "http://localhost:8080/users/login/";
const httpOptions: {} = {
headers: new HttpHeaders({
'ContentType': 'application/json'
})
};
return this.http.post<String>(tokenUrlPreFlight, model, httpOptions);
}
public sendToken(token) {
const tokenUrlPostFlight = "http://localhost:8080/rest/users/";
console.log("Bearer " + token);
const httpOptions: {} = {
headers: new HttpHeaders({
'Authorization': 'Bearer ' + token
})
};
return this.http.get(tokenUrlPostFlight, httpOptions);
}
public logout(): void {
localStorage.setItem("token", "");
localStorage.setItem("currentUserName", "");
alert("You just logged out");
}
public checkLogin(): boolean {
if(localStorage.getItem("currentUserName") != null && localStorage.getItem("currentUserName") != "" && localStorage.getItem("token") != null && localStorage.getItem("token") != "") {
console.log(localStorage.getItem("currentUserName"));
console.log(localStorage.getItem("token"));
return true;
}
return false;
}
}
And now for the java. First here is my entry point:
import com.acb.app.configuration.JwtFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
#SpringBootApplication
public class App {
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean<JwtFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new JwtFilter());
filterRegistrationBean.addUrlPatterns("/rest/*");
return filterRegistrationBean;
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
My UserController.java
import com.acb.app.model.User;
import io.jsonwebtoken.*;
import com.acb.maki.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
#RestController
#RequestMapping("/users")
public class UserController {
#Autowired
private UserService userService;
#GetMapping("")
public List<User> userIndex() {
return userService.getAllUsers();
}
#GetMapping("{username}")
public Optional<User> getUserByUsername(#RequestBody String username) {
return userService.findByUsername(username);
}
#PostMapping("login")
public String login(#RequestBody Map<String, String> json) throws ServletException {
if(json.get("username") == null || json.get("password") == null) {
throw new ServletException("Please fill in username and password");
}
final String username = json.get("username");
final String password = json.get("password");
Optional<User> optionalUser = userService.findByUsername(username);
if(!optionalUser.isPresent()) {
throw new ServletException("Username not found");
}
User user = optionalUser.get();
if(!password.equals(user.getPassword())) {
throw new ServletException("Invalid login. Please check username and password");
}
final String response = Jwts.builder().setSubject(username).claim("roles", "user").setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, "secretKey").compact();
final String jsonResponse = String.format("\"%s\"", response);
System.out.println(jsonResponse);
return jsonResponse;
}
#PostMapping(value="/register")
public User register(#RequestBody User user) {
return userService.save(user);
}
}
And last but not least my JwtFilter.java
import io.jsonwebtoken.*;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String authHeader = request.getHeader("Authorization");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(servletRequest, servletResponse);
} else {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid authorization header");
}
final String token = authHeader.substring(7);
try {
final JwtParser jwtParser = Jwts.parser();
final Jws<Claims> claimsJws = jwtParser.setSigningKey("secretKey").parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
I am very aware of the bad practices here with comparing plain text passwords, etc. I'm just trying to get this to work for the time being. The token is correctly generated and returned. The token is also set correctly to local storage, but upon making the get request to the /rest/users route a 404 is returned. There are no errors on the java side.
So as I expected, I am indeed an idiot. As the user mentioned above my service maps to /users/ and what I needed is the protected version /rest/users. So next to my UserController I created UserResource.java which looks like so:
import com.acb.app.model.User;
import com.acb.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
#RestController
#RequestMapping("/rest")
public class UserResource {
#Autowired
private UserService userService;
#RequestMapping("/users")
public List<User> findAllUsers() {
return userService.getAllUsers();
}
}
And this makes use of the JwtFilter and allows me to protect my routes. Hopefully this will be helpful to someone else.
How to access request headers in implementation of WriterInterceptor interface in JAX-RS?
context.getHeaders(); //This line gives a set of response headers(not request headers) in the WriterInterceptor implementation.
Complete code below:
public class GzipFilterWriterInterceptor implements WriterInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(GzipFilterWriterInterceptor.class);
#Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
MultivaluedMap<String,Object> headers = context.getHeaders();
headers.add("Content-Encoding", "gzip");
final OutputStream outputStream = context.getOutputStream();
context.setOutputStream(new GZIPOutputStream(outputStream));
context.proceed();
}
}
You can just inject HttpHeaders. It will be a thread-local proxy when it's injected, so it's thread safe.
#Context
private HttpHeaders headers;
It has methods
String getHeaderString(String name)
List<String> getRequestHeader(String name)
MultivaluedMap<String,String> getRequestHeaders()
UPDATE (test)
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;
import java.util.logging.Logger;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* Run like any other JUnit test. Only one required dependency:
*
* <dependency>
* <groupId>org.glassfish.jersey.test-framework.providers</groupId>
* <artifactId>jersey-test-framework-provider-inmemory</artifactId>
* <scope>test</scope>
* <version>${jersey.version}</version>
* </dependency>
*/
public class HeadersTest extends JerseyTest {
#Path("hello")
public static class HelloResource {
#GET
public String get() {
return "Hello";
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(HelloResource.class)
.register(HeaderWriter.class)
.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
}
#Provider
public static class HeaderWriter implements WriterInterceptor {
#Context
private HttpHeaders headers;
#Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
context.proceed();
final String header = headers.getHeaderString("X-Request-Header");
context.getHeaders().add("X-Response-Header", header);
}
}
#Test
public void doit() {
final Response response = target("hello").request()
.header("X-Request-Header", "BooYah")
.get();
assertThat(response.getHeaderString("X-Response-Header"), is("BooYah"));
}
}
You can implement below code to ,see working example at http://jerseyexample-ravikant.rhcloud.com/rest/jws/say/Hello
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
#Provider
public class SecurityInterceptor implements ContainerRequestFilter, ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext reqCtx, ContainerResponseContext respCtx) throws IOException {
long startTime=0;
System.out.println("Adding ProcessingTime in response headers");
if(reqCtx.getHeaderString("startTime")!=null)
startTime = Long.parseLong(reqCtx.getHeaderString("startTime"));
respCtx.getHeaders().add("ProcessingTime",
String.valueOf(System.currentTimeMillis() - startTime) + " millisecs");
}
#Override
public void filter(ContainerRequestContext reqCtx) throws IOException {
System.out.println("Adding start time in request headers");
reqCtx.getHeaders().add("startTime", String.valueOf(System.currentTimeMillis()));
}
}
#Provider
public class GzipFilterWriterInterceptor implements WriterInterceptor
{
private static final Logger LOG = LoggerFactory.getLogger(GzipFilterWriterInterceptor.class);
// use a context injection
#Context
private HttpHeaders httpHeaders;
#Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException
{
MultivaluedMap<String,Object> headers = context.getHeaders();
headers.add("Content-Encoding", "gzip");
// do stuff with headers
if ("Basic Ym9iOnBhc3N3b3Jk".equals(httpHeaders.getRequestHeader("Authorization").get(0)))
{
//do stuff here, but be careful about the indexoutofbounds...
}
final OutputStream outputStream = context.getOutputStream();
context.setOutputStream(new GZIPOutputStream(outputStream));
context.proceed();
}
}
The context injection will be injected per request, see the javadocs.
Probably not best solution, but you can have your interceptor implementing ReaderInterceptor. There you can get headers and save them in ThreadLocal variable so you can access then in WriterInterceptor
Also if you have annotation based configuration you can try to inject ContainerRequestContext with a #Context annotation
I want to send a soap message with Spring Integration. I use Java Config.
I've tried the flolowing interceptor, but the spring integration converts the angle brackets (<>) into html escape characters.
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapHeader;
import org.springframework.ws.soap.SoapHeaderElement;
import org.springframework.ws.soap.SoapMessage;
public class MyAuthInterceptor implements ClientInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
SoapMessage soapMessage = (SoapMessage) messageContext.getRequest();
SoapHeader sh = soapMessage.getSoapHeader();
QName name = new QName("http://...", "myAuth", "aut");
sh.addHeaderElement(name).setText("<username>TestUser</username>" + "<password>TestPass</password>");
return true;
}
Here is the generated soap header:
<SOAP-ENV:Header>
<aut:myAuth xmlns:aut="http://.../"><username>TestUser</username><password>TestPass</password></aut:myAuth>
</SOAP-ENV:Header>
Here is my configuration:
#Configuration
#EnableIntegration
public class SpringIntegrationConfiguration {
#Bean
public PublishSubscribeChannel inputChannel() {
return new PublishSubscribeChannel();
}
#Bean
public ClientInterceptor myAuthInterceptor() {
return new MyAuthInterceptor();
}
#Bean
#ServiceActivator(inputChannel = "inputChannel")
public SimpleWebServiceOutboundGateway myOutput(ClientInterceptor mekAuthInterceptor) {
SimpleWebServiceOutboundGateway simpleWebServiceOutboundGateway = new SimpleWebServiceOutboundGateway("http://...");
simpleWebServiceOutboundGateway.setInterceptors(myAuthInterceptor);
return simpleWebServiceOutboundGateway;
}
}
How can I set the soap header without escaping the angle brackets?
You have to build it up using addChildElements instead of setting it as text.
You are setting the tags as text which escapes the string since it is being added to an xml. Those needs to be set as elements
http://docs.oracle.com/javaee/5/api/javax/xml/soap/SOAPHeaderElement.html
Check the methods in the doc above and use it appropriately. Leave a comment in case you need more help.
#gary-russell - Your answer made sense from a logical point of view, but there are no "addChildElement" method in org.springframework.ws.soap.. I found them in javax.xml.soap..
So my result looks like the following:
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessage;
public class MySecurityInterceptor implements ClientInterceptor
{
private static final Logger log = LoggerFactory.getLogger(MySecurityInterceptor.class);
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException
{
SoapMessage soapMessage = (SoapMessage) messageContext.getRequest();
QName securityName = new QName(
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
"Security",
XMLConstants.DEFAULT_NS_PREFIX);
QName usernameToken = new QName(null, "UsernameToken", XMLConstants.DEFAULT_NS_PREFIX);
QName username = new QName(null, "Username", XMLConstants.DEFAULT_NS_PREFIX);
QName password = new QName(null, "Password", XMLConstants.DEFAULT_NS_PREFIX);
try
{
SOAPMessage mySoapMessage = ((SaajSoapMessage) soapMessage).getSaajMessage();
SOAPHeader header = mySoapMessage.getSOAPHeader();
SOAPHeaderElement securityElement = header.addHeaderElement(securityName);
SOAPElement usernameTokenElement = securityElement.addChildElement(usernameToken);
SOAPElement usernameElement = usernameTokenElement.addChildElement(username);
SOAPElement passwordElement = usernameTokenElement.addChildElement(password);
usernameElement.setTextContent("jxep-Zenmonics-1-i101#jxtest.local");
passwordElement.setTextContent("3Sg%T~1q4z!QnH6#+5pD");
}
catch (SOAPException e)
{
log.error("Error!", e);
}
return true;
}
#Override
public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException
{
// Auto-generated method stub
return false;
}
#Override
public boolean handleFault(MessageContext messageContext) throws WebServiceClientException
{
// Auto-generated method stub
return false;
}
#Override
public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException
{
// Auto-generated method stub
}
}