Read another parameter within jersey's ParamConverter - java

I've made a ParamConverter which provides an Instant (Date) when given a string formatted as either Instant's native ISO-8601, or as an integer number of milliseconds since the epoch. This is working fine, but I also need to be able to support other date formats (the customers are fussy).
To avoid the classic dd/mm/yyyy vs mm/dd/yyyy ambiguity, I'd like to have the customer specify their preferred format as part of the request*. e.g:
GET http://api.example.com/filter?since=01/02/2000&dateformat=dd/mm/yyyy
passed to a method which looks like:
#GET
String getFilteredList( final #QueryParam( "since" ) Instant since ) {
...
}
(time & timezone parts omitted for clarity)
So I'd like my ParamConverter<Instant> to be able to read the dateformat parameter.
I've been able to use a combination of a filter which sets a ContainerRequestContext property and an AbstractValueFactoryProvider to do something similar, but that needs the parameter to have a custom annotation applied and doesn't let it work with QueryParam/FormParam/etc., making it far less useful.
Is there any way to get other parameters, or the request object itself, from inside a ParamConverter?
[*] In the real world this would be from a selection of pre-approved formats, but for now just assume they're providing the input to a DateTimeFormatter
For clarity, here's the code I have:
public class InstantParameterProvider implements ParamConverterProvider {
private static final ParamConverter<Instant> INSTANT_CONVERTER =
new ParamConverter<Instant>( ) {
#Override public final T fromString( final String value ) {
// This is where I would like to get the other parameter's value
// Is it possible?
}
#Override public final String toString( final T value ) {
return value.toString( );
}
};
#SuppressWarnings( "unchecked" )
#Override public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations
) {
if( rawType == Instant.class ) {
return (ParamConverter<T>) INSTANT_CONVERTER;
}
return null;
}
}

As mentioned here, the key to this is injecting some context object with javax.inject.Provider, which allows us to retrieve the object lazily. Since the ParamConverterProvider is a component managed by Jersey, we should be able to inject other components.
The problem is that the component we need is going to be in a request scope. To get around that, we inject javax.inject.Provider<UriInfo> into the provider. When we actually call get() in the Provider to get the actual instance of UriInfo, it will be be in a request. The same goes for any other component that requires a request scope.
For example
public class InstantParamProvider implements ParamConverterProvider {
#Inject
private javax.inject.Provider<UriInfo> uriInfoProvider;
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType,
Type genericType,
Annotation[] annotations) {
if (rawType != Instant.class) return null;
return new ParamConverter<T>() {
#Override
public T fromString(String value) {
UriInfo uriInfo = uriInfoProvider.get();
String format = uriInfo.getQueryParameters().getFirst("date-format");
if (format == null) {
throw new WebApplicationException(Response.status(400)
.entity("data-format query parameter required").build());
} else {
try {
// parse and return here
} catch (Exception ex) {
throw new WebApplicationException(
Response.status(400).entity("Bad format " + format).build());
}
}
}
#Override
public String toString(T value) {
return value.toString();
}
};
}
}
UPDATE
Here is a complete example using Jersey Test Framework
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.junit.matchers.JUnitMatchers.*;
public class LocalDateTest extends JerseyTest {
public static class LocalDateParamProvider implements ParamConverterProvider {
#Inject
private javax.inject.Provider<UriInfo> uriInfoProvider;
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType,
Type genericType,
Annotation[] annotations) {
if (rawType != LocalDate.class) {
return null;
}
return new ParamConverter<T>() {
#Override
public T fromString(String value) {
UriInfo uriInfo = uriInfoProvider.get();
String format = uriInfo.getQueryParameters().getFirst("date-format");
if (format == null) {
throw new WebApplicationException(Response.status(400)
.entity("date-format query parameter required").build());
} else {
try {
return (T) LocalDate.parse(value, DateTimeFormatter.ofPattern(format));
// parse and return here
} catch (Exception ex) {
throw new WebApplicationException(
Response.status(400).entity("Bad format " + format).build());
}
}
}
#Override
public String toString(T value) {
return value.toString();
}
};
}
}
#Path("localdate")
public static class LocalDateResource {
#GET
public String get(#QueryParam("since") LocalDate since) {
return since.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"));
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(LocalDateResource.class)
.register(LocalDateParamProvider.class)
.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
}
#Test
public void should_return_bad_request_with_bad_format() {
Response response = target("localdate")
.queryParam("since", "09/20/2015")
.queryParam("date-format", "yyyy/MM/dd")
.request().get();
assertEquals(400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("format yyyy/MM/dd"));
response.close();
}
#Test
public void should_return_bad_request_with_no_date_format() {
Response response = target("localdate")
.queryParam("since", "09/20/2015")
.request().get();
assertEquals(400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("query parameter required"));
response.close();
}
#Test
public void should_succeed_with_correct_format() {
Response response = target("localdate")
.queryParam("since", "09/20/2015")
.queryParam("date-format", "MM/dd/yyyy")
.request().get();
assertEquals(200, response.getStatus());
assertThat(response.readEntity(String.class), containsString("09/20/2015"));
response.close();
}
}
Here's the test dependency
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>

Related

What is the use of groups and payload in custom Annotation In Java?

Here's My code ,
I tried reading many stuff online but was not able to understand actual use of
Class<?> [] groups() default{};
Class<? extends Payload>[] payload() default{};
Here is my code in which i have used it.
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UniqueValidator.class)
public #interface UniqueValue {
String message() default "Unique Constraint Violated";
Class<?> [] groups() default{};
Class<? extends Payload>[] payload() default{};
}
Thank you.
Groups allow you to restrict the set of constraints applied during validation.
In the following example, the AClass class has the bool attribute that belongs to the AGroup group. But when the group of an attribute is not specified, which is the case of the other attributes of AClass, then it belongs to the default group javax.validation.groups.Default.
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
public class AClass {
#NotBlank
private String string;
#Min(2)
private int integer;
#AssertTrue(groups = AGroup.class)
private boolean bool;
public AClass(String string, int integer, boolean bool) {
this.string = string;
this.integer = integer;
this.bool = bool;
}
}
A group declaration can be a simple empty interface.
public interface AGroup { }
In the following program, the validate method validates all attributes that belong to the default group. But to validate attributes of other groups, you must specify in the validate method which group of attributes will be validated.
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import org.junit.Assert;
public class Program {
public static void main(String[] args) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
// create a AClass and check that everything is ok with it.
AClass a = new AClass("Foo", 2, false);
Set<ConstraintViolation<AClass>> constraintViolations = validator.validate(a);
Assert.assertEquals(0, constraintViolations.size());
// but has it passed the bool field?
constraintViolations = validator.validate(a, AGroup.class);
Assert.assertEquals(1, constraintViolations.size());
Assert.assertEquals("must be true", constraintViolations.iterator().next().getMessage()
);
}
}
Source: https://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/ch05.html#example-driver
Payloads can be used by clients of the Bean Validation API to assign custom payload objects to a constraint. This attribute is not used by the API itself.
In the following example, the TestBean class has an attribute with Payload.
import javax.validation.constraints.NotNull;
public class TestBean {
#NotNull(payload = {ErrorEmailSender.class})
private String str;
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
}
Implementation of ErrorEmailSender Payload:
import javax.validation.ConstraintViolation;
import javax.validation.Payload;
public class ErrorEmailSender implements Payload {
public void onError(ConstraintViolation violation) {
System.out.println("Sending email to support team: "
+ violation.getPropertyPath() + " "
+ violation.getMessage());
}
}
The Client program checks whether a TestBean instance is valid, and if not, it calls the Payload of invalid attributes.
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
public class Client {
public static void main(String[] args) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
TestBean bean = new TestBean();
Set<ConstraintViolation<TestBean>> constraintViolations = validator.validate(bean);
if (constraintViolations.size() > 0) {
constraintViolations.stream().forEach(Client::processError);
} else {
//proceed using user object
System.out.println(bean);
}
}
private static void processError(ConstraintViolation<TestBean> violation) {
violation.getConstraintDescriptor().getPayload().forEach(p -> {
if (ErrorEmailSender.class.isAssignableFrom(p)) {
try {
((ErrorEmailSender) p.getDeclaredConstructor().newInstance()).onError(violation);
} catch (Exception ex) {
System.out.println(ex);
}
}
});
}
}
You should see in the output:
Sending email to support team: str must not be null
Sources:
https://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/ch06.html
https://www.logicbig.com/tutorials/java-ee-tutorial/bean-validation/constraint-payload.html

Can I make it mandatory to include at least one of two headers with Spring?

One of my headers is misspelled, and I want to change it while being backwards compatible.
#RequestHeader(value = "Custmer-Key") String customerKey
I want to add a header with the correct spelling Customer-Key, and make at least one of them mandatory. Any ideas?
I'll make a few assumptions here. Each one may or may not be correct in your specific case, but the purpose is to give better context on when such solution is viable and makes sense to use.
You have a need to keep backward compatibility (this one is easy... you wrote it)
You have a pretty large codebase possibly based on microservices and maintained by several developers and you want to avoid large commits spanning across several teams, centralising the fix in a common shared library that all services are meant to use
Your headers are fetched using not just Spring but occasionally also by accessing the request directly
You are working in a production application where you want to change as little code as possible as some of its inner workings are difficult to understand
The solution consists into wiring a custom filter, along with its configuration. The filter will swap the HttpServletRequest instance with a different one that allows to manipulate the headers.
First, create your own filter, as follows:
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE)
public class HeadersFilter implements Filter {
private static final String WRONG_HEADER = "Custmer-Key";
private static final String RIGHT_HEADER = "Customer-Key";
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String newHeaderValue = request.getHeader(RIGHT_HEADER);
String headerValue;
if(newHeaderValue != null) {
headerValue = newHeaderValue;
}
else {
headerValue = request.getHeader(WRONG_HEADER);
}
HeadersRewriteHttpServletRequestWrapper requestWrapper = new HeadersRewriteHttpServletRequestWrapper(request);
requestWrapper.setCustomHeader(WRONG_HEADER, headerValue);
filterChain.doFilter(requestWrapper, response);
}
public static class HeadersRewriteHttpServletRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> customHeaders;
HeadersRewriteHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
customHeaders = new HashMap<>();
}
void setCustomHeader(String name, String value) {
customHeaders.put(name, value);
}
private String getCustomHeader(String name) {
return customHeaders.get(name);
}
#Override
public String getHeader(String name) { // not needed by spring but useful if someone uses this method directly
String header = super.getHeader(name);
if(header != null) {
return header;
}
return getCustomHeader(name);
}
#Override
public Enumeration<String> getHeaderNames() {
Set<String> names = new HashSet<>(Collections.list(super.getHeaderNames()));
names.addAll(customHeaders.keySet());
return Collections.enumeration(names);
}
#Override
public Enumeration<String> getHeaders(String name) {
List<String> headers = Collections.list(super.getHeaders(name));
String customHeader = getCustomHeader(name);
if(headers.isEmpty() && customHeader != null) {
headers.add(customHeader);
}
return Collections.enumeration(headers);
}
}
}
Second, wire in the Spring configuration to create an instance of this filter and inject it as necessary.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class FilterConfiguration {
#Bean
public HeadersFilter headersFilterBean() {
return new HeadersFilter();
}
}
That's it. Assuming your application doesn't have quirks preventing this from working (in which case good luck with your debugging), this code will take the contents of both Customer-Key and Custmer-Key, giving precedence to Customer-Key and write them in a fake Custmer-Key header. This way you won't have to touch any of the controllers, which should continue to work transparently.
Next Approach is to create a annotation OneOf or something. I have used a simpler approach than using Aspect. Using this approach you can validate request param , Requestbody and RequestHeader
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = OneOfValidator.class)
#Documented
public #interface OneOf {
String message() default "";
String[] value();
}
Create a validator class like below.
public class OneOfValidator implements ConstraintValidator<OneOf, Object> {
private String[] fields;
private String fieldList;
public void initialize(OneOf annotation) {
this.fields = annotation.value();
fieldList = Arrays.toString(fields);
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
int matches = countNumberOfMatches(wrapper);
if (matches > 1) {
setErrorMessage(context, <your message>);
return false;
} else if (matches == 0) {
setErrorMessage(context, <your message>);
return false;
}
return true;
}
private int countNumberOfMatches(BeanWrapper wrapper) {
int matches = 0;
for (String field : fields) {
Object value = wrapper.getPropertyValue(field);
boolean isPresent = detectOptionalValue(value);
if (value != null && isPresent) {
matches++;
}
}
return matches;
}
private boolean detectOptionalValue(Object value) {
if (value instanceof Optional) {
return ((Optional)value).isPresent();
}
if (value instanceof String) {
return StringUtils.hasText((String)value);
}
return true;
}
private void setErrorMessage(ConstraintValidatorContext context, String template) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate(template)
.addNode(fieldList)
.addConstraintViolation();
}
In the controller you can create something like below.
#GetMapping(value = "your path")
public ResponseEntity<HeaderDataDTO> getBuildDetails(#RequestHeader(value = "Custmer-Key") String custmerKey,#RequestHeader(value = "Customer-Key") String customerKey
) {
HeaderDataDTO data = new HeaderDataDTO();
data.setCustomerKey(customerKey);
data.setCustmerKey(custmerKey);
data.validate();
return new ResponseEntity<>(data,
HttpStatus.OK);
}
You can define your DTO as below.
#Valid
#OneOf(value = {"customerKey", "custmerKey"})
public class HeaderDataDTO extends HeaderValidator {
private String customerKey;
private String custmerKey;
//getter and setter
HeaderValidator should be like below. Validate method will validate the object.
import org.springframework.util.CollectionUtils;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
public abstract class HeaderValidator {
public boolean validate() {
Validator validator = Validation
.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<HeaderValidator>> violations = validator.validate(this);
if (!CollectionUtils.isEmpty(violations)) {
throw <your exception>
}
return true;
}
You can create a interceptor like below.
#Component
#Primary
public class HeadersInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpInputMessage inputMessage=new ServletServerHttpRequest(request);
HttpHeaders httpHeaders = inputMessage.getHeaders();
//validation code for header goes here.
//return true if validation is successful
return true;
}
}
and add the interceptor to your configuration.
#Configuration
public class InterceptorConfig implements WebMvcConfigurer {
#Autowired
HeadersInterceptor headersInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(headersInterceptor);
}
}`
Now you can customize your validation in any manner.

Mask json fields using jackson

I am trying to mask sensitive data while serializing using jackson.
I have tried using #JsonSerialize and a custom annotation #Mask .
Mask.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface Mask {
String value() default "XXX-DEFAULT MASK FORMAT-XXX";
}
Employee.java
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.util.Map;
public class Employee {
#Mask(value = "*** The value of this attribute is masked for security reason ***")
#JsonSerialize(using = MaskStringValueSerializer.class)
protected String name;
#Mask
#JsonSerialize(using = MaskStringValueSerializer.class)
protected String empId;
#JsonSerialize(using = MaskMapStringValueSerializer.class)
protected Map<Category, String> categoryMap;
public Employee() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmpId() {
return empId;
}
public void setEmpId(String empId) {
this.empId = empId;
}
public Map<Category, String> getCategoryMap() {
return categoryMap;
}
public void setCategoryMap(Map<Category, String> categoryMap) {
this.categoryMap = categoryMap;
}
}
Category.java
public enum Category {
#Mask
CATEGORY1,
#Mask(value = "*** This value of this attribute is masked for security reason ***")
CATEGORY2,
CATEGORY3;
}
MaskMapStringValueSerializer.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Map;
public class MaskMapStringValueSerializer extends JsonSerializer<Map<Category, String>> {
#Override
public void serialize(Map<Category, String> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
for (Category key : map.keySet()) {
Mask annot = null;
try {
annot = key.getClass().getField(key.name()).getAnnotation(Mask.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (annot != null) {
jsonGenerator.writeStringField(((Category) key).name(), annot.value());
} else {
jsonGenerator.writeObjectField(((Category) key).name(), map.get(key));
}
}
jsonGenerator.writeEndObject();
}
}
MaskStringValueSerializer.java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class MaskStringValueSerializer extends StdSerializer<String> implements ContextualSerializer {
private Mask annot;
public MaskStringValueSerializer() {
super(String.class);
}
public MaskStringValueSerializer(Mask logMaskAnnotation) {
super(String.class);
this.annot = logMaskAnnotation;
}
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (annot != null && s != null && !s.isEmpty()) {
jsonGenerator.writeString(annot.value());
} else {
jsonGenerator.writeString(s);
}
}
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
Mask annot = null;
if (beanProperty != null) {
annot = beanProperty.getAnnotation(Mask.class);
}
return new MaskStringValueSerializer(annot);
}
}
MaskValueTest.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class MaskValueTest {
public static void main(String args[]) throws Exception{
Employee employee = new Employee();
employee.setName("John Doe");
employee.setEmpId("1234567890");
Map<Category, String> catMap = new HashMap<>();
catMap.put(Category.CATEGORY1, "CATEGORY1");
catMap.put(Category.CATEGORY2, "CATEGORY2");
catMap.put(Category.CATEGORY3, "CATEGORY3");
employee.setCategoryMap(catMap);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
}
}
Output -
{
"name" : "*** The value of this attribute is masked for security reason ***",
"empId" : "XXX-DEFAULT MASK FORMAT-XXX",
"categoryMap" : {
"CATEGORY1" : "XXX-DEFAULT MASK FORMAT-XXX",
"CATEGORY2" : "*** The value of this attribute is masked for security reason ***",
"CATEGORY3" : "CATEGORY3"
}
}
The result is as per expectation, however, this seems to be static masking.
The intention was to mask only when needed, e.g. while printing in the logs where the all these sensitive data should be masked.
If I have to send this json for document indexing where the values should be as it is, this implementation fails.
I am looking for an Annotation based solution, where I can use 2 different instance of ObjectMapper initialized with JsonSerializers.
This can be an implementation for what Andreas suggested:
create a class MaskAnnotationIntrospector which extend from JacksonAnnotationIntrospector and override its findSerializer method, like this:
public class MaskAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findSerializer(Annotated am) {
Mask annotation = am.getAnnotation(Mask.class);
if (annotation != null)
return MaskingSerializer.class;
return super.findSerializer(am);
}
}
Therefore, you can have two instance of ObjectMapper. Add MaskAnnotationIntrospector to the one in which you want to Mask (e.g. for logging purpose):
mapper.setAnnotationIntrospector(new MaskAnnotationIntrospector());
The other instance which MaskAnnotationIntrospector has not set into it, do not mask any during serialization.
P.S. MaskAnnotationIntrospector can be extended from both JacksonAnnotationIntrospector & NopAnnotationIntrospector, but the latter does not provide any implementation for findSerializer method and calling super.findSerializer(am) simply return null and as a direct result, other Jackson annotation (such as #JsonIgnore) discarded, but by using the former, this problem solved
Remove the #JsonSerialize annotations, and put the logic of how to handle the #Mask annotation in a Module, e.g. have it add an AnnotationIntrospector.
You can now choose whether or not to call registerModule(Module module).
As for writing the module, I'll leave that up to you. If you have any questions about that, ask another Question.
Instead of having MaskStringValueSerializer.java you can create module to bundle the serializer and register the module with objectmapper whenever you want , which will eventually allow you to have two different instances of objectmapper.
Create a module to bundle the serializer
public class MaskingModule extends SimpleModule {
private static final String NAME = "CustomIntervalModule";
private static final VersionUtil VERSION_UTIL = new VersionUtil() {};
public MaskingModule() {
super(NAME, VERSION_UTIL.version());
addSerializer(MyBean.class, new MaskMapStringValueSerializer());
}
}
Register the module with ObjectMapper and use it
ObjectMapper objectMapper = new ObjectMapper().registerModule(new MaskingModule());
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
Also you can extend the Object Mapper , register the module and use it
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
registerModule(new MaskingModule());
}
}
CustomObjectMapper customObjectMapper = new CustomObjectMapper ();
System.out.println(customObjectMapper .writerWithDefaultPrettyPrinter().writeValueAsString(employee));
why don't you use two parameters one for original value and one for masked value. For example in this case you can use String name and String maskedName. then for logging you can use masked value.

Passing #Context argument to method in class

I have an existing class I'm trying to hook into to get some header parameters to SSO a user into our system. The class is as follows.
import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import org.springframework.stereotype.Component;
#Component
#Path("/http")
public class HttpResource {
#GET
#Path("/getHeaders")
#Produces(MediaType.APPLICATION_JSON)
public Map<String, String> getAllHeaders(#Context HttpHeaders headers) {
Map<String, String> headerList = new HashMap<String, String>();
for (String key : headers.getRequestHeaders().keySet()) {
String value = headers.getRequestHeader(key).get(0);
System.out.println(key + " : " + value);
headerList.put(key, value);
}
return headerList;
}
}
What I'm trying to figure out is how do I call getAllHeaders() with the #Context argument? I've found a ton of examples of the class I have, but nothing that shows how to call it.
I've also tried putting the annotation inside the class instead of as an argument.
#Context
HttpHeaders httpHeaders;
but when I try to access httpHeaders.getAllHeaders() it returns null. I assume because it's not actually created because the javax documents say it will never return null.
I'm trying to call this within my SSOAuthorizationFilter.java, but have also tried accessing it via a controller as well.
Write an Annotation first.
#Retention(RUNTIME)
#Target({ PARAMETER })
#Documented
public #interface SSOAuthorization {}
And then a Resolver for that.
public class SSOAuthorizationResolver {
public static class SSOAuthorizationInjectionResolver extends
ParamInjectionResolver<SSOAuthorization> {
public SSOAuthorizationInjectionResolver() {
super(SSOAuthorizationValueFactoryProvider.class);
}
}
#Singleton
public static class SSOAuthorizationValueFactoryProvider extends
AbstractValueFactoryProvider {
#Context
private HttpHeaders httpHeaders;
#Inject
public SSOAuthorizationValueFactoryProvider(
final MultivaluedParameterExtractorProvider mpep,
final ServiceLocator injector) {
super(mpep, injector, Parameter.Source.UNKNOWN);
}
#Override
protected Factory<?> createValueFactory(final Parameter parameter) {
final Class<?> classType = parameter.getRawType();
if (!Language.class.equals(classType)
|| parameter.getAnnotation(SSOAuthorization.class) == null) {
return null;
}
return new AbstractContainerRequestValueFactory<String>() {
#Override
public String provide() {
// Can use httpHeader to get your header here.
return httpHeader.getHeaderString("SSOAuthorization");
}
};
}
}
// Binder implementation
public static class Binder extends AbstractBinder {
#Override
protected void configure() {
bind(SSOAuthorizationValueFactoryProvider.class).to(
ValueFactoryProvider.class).in(Singleton.class);
bind(SSOAuthorizationInjectionResolver.class).to(
new TypeLiteral<InjectionResolver<SSOAuthorization>>() {
}).in(Singleton.class);
}
}
}
And in the ResourceConfig just register the Resolver
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig(Class... classes) {
super(classes);
register(new SSOAuthorizationResolver.Binder());
}
}
And finally use it in your controller with the #SSOAuthorization annotation.
#GET
#Path("/get")
#Produces(MediaType.APPLICATION_JSON)
public String someMethod(#SSOAuthorization String auth) {
return auth;
}

Xstream's jodatime Local Date display

I'm using xstrem to serialise a jodatime local date into xml.
However when output the generated xml the LocalDate is not in an easily readable format.
See below:
<date>
<iLocalMillis>1316563200000</iLocalMillis>
<iChronology class="org.joda.time.chrono.ISOChronology" reference="../../tradeDate/iChronology"/>
Any ideas how I can get xstream to display the date in a format that won't drive me up the wall?
Here's what I have used successfully. I believe I used the info at the link mentioned in the first post.
import java.lang.reflect.Constructor;
import org.joda.time.DateTime;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
public final class JodaTimeConverter implements Converter {
#Override
#SuppressWarnings("unchecked")
public boolean canConvert(final Class type) {
return (type != null) && DateTime.class.getPackage().equals(type.getPackage());
}
#Override
public void marshal(final Object source, final HierarchicalStreamWriter writer,
final MarshallingContext context) {
writer.setValue(source.toString());
}
#Override
#SuppressWarnings("unchecked")
public Object unmarshal(final HierarchicalStreamReader reader,
final UnmarshallingContext context) {
try {
final Class requiredType = context.getRequiredType();
final Constructor constructor = requiredType.getConstructor(Object.class);
return constructor.newInstance(reader.getValue());
} catch (final Exception e) {
throw new RuntimeException(String.format(
"Exception while deserializing a Joda Time object: %s", context.getRequiredType().getSimpleName()), e);
}
}
}
You can register it like:
XStream xstream = new XStream(new StaxDriver());
xstream.registerConverter(new JodaTimeConverter());
The version from #Ben Carlson has an issue if your object tree contains other classes from the same package as DateTime.
A more robust version for converting DateTime to XML and back that does not require reflection as well:
public static class JodaTimeConverter implements Converter
{
#Override
#SuppressWarnings("unchecked")
public boolean canConvert( final Class type )
{
return DateTime.class.isAssignableFrom( type );
}
#Override
public void marshal( Object source, HierarchicalStreamWriter writer, MarshallingContext context )
{
writer.setValue( source.toString() );
}
#Override
#SuppressWarnings("unchecked")
public Object unmarshal( HierarchicalStreamReader reader,
UnmarshallingContext context )
{
return new DateTime( reader.getValue() );
}
}
Register the converter with XStream to use it:
XStream xstream = new XStream();
xstream.registerConverter(new JodaTimeConverter());
We needed a to convert a Joda DateTime to / from an XML attribute. For that, converters need to implement interface SingleValueConverter. Our final implementation:
package com.squins.xstream.joda;
import org.joda.time.DateTime;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter;
public final class JodaDateTimeConverter extends AbstractSingleValueConverter
{
#Override
public boolean canConvert(final Class type)
{
return DateTime.class.equals(type);
}
#Override
public Object fromString(String str)
{
try
{
return new DateTime(str);
}
catch (final Exception e)
{
throw new ConversionException("Cannot parse date " + str);
}
}
}
You have to implement (or find) a custom converter for xstream, which will handle JodaTime object in a way you find appropriate.
Here is a small example of such converter: http://x-stream.github.io/converter-tutorial.html
I've used the one that it is here. Pasting it for simplicity:
public class JodaTimeConverter implements Converter
{
#Override
public boolean canConvert(Class type) {
return type != null && DateTime.class.getPackage().equals(type.getPackage());
}
#Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
writer.setValue(source.toString());
}
#Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
try {
Constructor constructor = context.getRequiredType().getConstructor(Object.class);
return constructor.newInstance(reader.getValue());
} catch (Exception e) { // NOSONAR
throw new SerializationException(String.format(
"An exception occurred while deserializing a Joda Time object: %s",
context.getRequiredType().getSimpleName()), e);
}
}
}
The other samples didn't work.
Cheers!

Categories