I have a POJO:
class Test {
private int i;
void setI(int i) {
this.i = i;
}
}
this is what I have so far for the aspect:
public aspect t perthis(within(#Tracking *)){
private Set<String> set = new HashSet<>();
pointcut setterMethod() : execution(public void set*(..));
after(Object o) returning() : setterMethod() && this(o) {
set.add(thisJoinPoint.getSignature().getName());
System.out.println(set);
}
public Set<String> go() {
return set;
}
}
I want a Set<String> set for any instance of ANY class that has #Tracking. I also want to add the go() method for any instance of ANY class that has #Tracking.
Can't figure out the syntax. The go() method doesn't get added. If I put Test.go() then the method get added, but then it crashes during runtime.
Marker interface:
package de.scrum_master.tracking;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
#Retention(RUNTIME)
#Target(TYPE)
public #interface Track {}
Sample POJOs with/without marker annotation:
We use two sample classes for positive/negative tests.
package de.scrum_master.app;
import de.scrum_master.tracking.Track;
#Track
class TestTracked {
private int number;
private String text;
public void setNumber(int number) {
this.number = number;
}
public void setText(String text) {
this.text = text;
}
#Override
public String toString() {
return "TestTracked [number=" + number + ", text=" + text + "]";
}
}
package de.scrum_master.app;
class TestUntracked {
private int number;
private String text;
public void setNumber(int number) {
this.number = number;
}
public void setText(String text) {
this.text = text;
}
#Override
public String toString() {
return "TestUntracked [number=" + number + ", text=" + text + "]";
}
}
"Dirty tracker" interface:
We want the aspect to implement the following interface for each class annotated by #Track by means of inter-type declaration (ITD).
package de.scrum_master.tracking;
import java.util.Set;
public interface Trackable {
Set<String> getDirty();
}
Driver application:
Here, we are assuming that each POJO class annotated with the marker interface automagically implements the Trackable interface and therefore knows the getDirty() method, which the we call in order to verify that the aspect correctly tracks setter calls.
package de.scrum_master.app;
import de.scrum_master.tracking.Trackable;
public class Application {
public static void main(String[] args) {
TestTracked testTracked = new TestTracked();
testTracked.setNumber(11);
testTracked.setText("foo");
System.out.println(testTracked);
if (testTracked instanceof Trackable)
System.out.println("Dirty members: " + ((Trackable) testTracked).getDirty());
TestUntracked testUntracked = new TestUntracked();
testUntracked.setNumber(22);
testUntracked.setText("bar");
System.out.println(testUntracked);
if (testUntracked instanceof Trackable)
System.out.println("Dirty members: " + ((Trackable) testUntracked).getDirty());
}
}
Aspect:
This aspect makes each #Track-annotated class implement interface Trackable and provides both a private field storing tracking information and a getDirty() method implementation returning its value. Furthermore, the aspect makes sure to store the "dirty" information for each successfully executed setter.
package de.scrum_master.tracking;
import java.util.HashSet;
import java.util.Set;
public aspect TrackingAspect {
private Set<String> Trackable.dirty = new HashSet<>();
public Set<String> Trackable.getDirty() {
return dirty;
}
declare parents : #Track * implements Trackable;
pointcut setterMethod() : execution(public void set*(..));
after(Trackable trackable) returning() : setterMethod() && this(trackable) {
System.out.println(thisJoinPoint);
trackable.dirty.add(thisJoinPoint.getSignature().getName().substring(3));
}
}
Console log:
You will see this when running the driver application:
execution(void de.scrum_master.app.TestTracked.setNumber(int))
execution(void de.scrum_master.app.TestTracked.setText(String))
TestTracked [number=11, text=foo]
Dirty members: [Number, Text]
TestUntracked [number=22, text=bar]
The reason why we do not need perthis or pertarget instantiation is that we store the "dirty" information right inside the original classes. Alternatively, we could use per* instantiation and keep all information inside the corresponding aspect instances instead of using ITD. In that case however, the "dirty" information would be unaccessible from outside the aspect, which might even be desirable with regard to encapsulation. But then, whatever action needs to be performed when storing the "dirty" instances, would also need to happen from inside the aspect. As you did not provide an MCVE and hence your question is lacking detail, I did not consider this functionality in the aspect. I can easily imagine how this could be done with both aspect variants - per* instantiation vs. ITD - but I hate to speculate and then be wrong.
Related
I have a spring boot app, and I want to send DTO validation constraints as well as field value to the client.
Having DTO
class PetDTO {
#Length(min=5, max=15)
String name;
}
where name happens to be 'Leviathan', should result in this JSON being sent to client:
{
name: 'Leviathan'
name_constraint: { type: 'length', min:5, max: 15},
}
Reasoning is to have single source of truth for validations. Can this be done with reasonable amount of work?
To extend Frederik's answer I'll show a little sample code that convers an object to map and serializes it.
So here is the User pojo:
import org.hibernate.validator.constraints.Length;
public class User {
private String name;
public User(String name) {
this.name = name;
}
#Length(min = 5, max = 15)
public String getName() {
return name;
}
}
Then the actual serializer:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.util.ReflectionUtils;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
public class UserSerializer extends StdSerializer<User> {
public UserSerializer(){
this(User.class);
}
private UserSerializer(Class t) {
super(t);
}
#Override
public void serialize(User bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> properties = beanProperties(bean);
gen.writeStartObject();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
gen.writeObjectField(entry.getKey(), entry.getValue());
}
gen.writeEndObject();
}
private static Map<String, Object> beanProperties(Object bean) {
try {
return Arrays.stream(Introspector.getBeanInfo(bean.getClass(), Object.class).getPropertyDescriptors())
.filter(descriptor -> Objects.nonNull(descriptor.getReadMethod()))
.flatMap(descriptor -> {
String name = descriptor.getName();
Method getter = descriptor.getReadMethod();
Object value = ReflectionUtils.invokeMethod(getter, bean);
Property originalProperty = new Property(name, value);
Stream<Property> constraintProperties = Stream.of(getter.getAnnotations())
.map(anno -> new Property(name + "_constraint", annotationProperties(anno)));
return Stream.concat(Stream.of(originalProperty), constraintProperties);
})
.collect(toMap(Property::getName, Property::getValue));
} catch (Exception e) {
return Collections.emptyMap();
}
}
// Methods from Annotation.class
private static List<String> EXCLUDED_ANNO_NAMES = Arrays.asList("toString", "equals", "hashCode", "annotationType");
private static Map<String, Object> annotationProperties(Annotation anno) {
try {
Stream<Property> annoProps = Arrays.stream(Introspector.getBeanInfo(anno.getClass(), Proxy.class).getMethodDescriptors())
.filter(descriptor -> !EXCLUDED_ANNO_NAMES.contains(descriptor.getName()))
.map(descriptor -> {
String name = descriptor.getName();
Method method = descriptor.getMethod();
Object value = ReflectionUtils.invokeMethod(method, anno);
return new Property(name, value);
});
Stream<Property> type = Stream.of(new Property("type", anno.annotationType().getName()));
return Stream.concat(type, annoProps).collect(toMap(Property::getName, Property::getValue));
} catch (IntrospectionException e) {
return Collections.emptyMap();
}
}
private static class Property {
private String name;
private Object value;
public Property(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
}
And finally we need to register this serializer to be used by Jackson:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication(scanBasePackages = "sample.spring.serialization")
public class SerializationApp {
#Bean
public Jackson2ObjectMapperBuilder mapperBuilder(){
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
jackson2ObjectMapperBuilder.serializers(new UserSerializer());
return jackson2ObjectMapperBuilder;
}
public static void main(String[] args) {
SpringApplication.run(SerializationApp.class, args);
}
}
#RestController
class SerializationController {
#GetMapping("/user")
public User user() {
return new User("sample");
}
}
The Json that will be emitted:
{
"name_constraint":{
"min":5,
"max":15,
"payload":[],
"groups":[],
"message":"{org.hibernate.validator.constraints.Length.message}",
"type":"org.hibernate.validator.constraints.Length"
},
"name":"sample"
}
Hope this helps. Good luck.
You can always use a custom Jackson Serializer for this. Plenty of docs to do this can be found on the internet, might look something like this:
public void serialize(PetDTO value, JsonGenerator jgen, ...) {
jgen.writeStartObject();
jgen.writeNumberField("name", value.name);
jgen.writeObjectField("name_consteaint", getConstraintValue(value));
}
public ConstaintDTO getConstraintValue(PetDTO value) {
// Use reflection to check if the name field on the PetDTO is annotated
// and extract the min, max and type values from the annotation
return new ConstaintDTO().withMaxValue(...).withMinValue(...).ofType(...);
}
You may want to create a base-DTO class for which the converter kicks in so you don't have to create a custom converter for all your domain objects that need to expose the constraints.
By combining reflection and smart use of writing fields, you can get close. Downside is you can't take advantage of the #JsonXXX annotations on your domain objects, since you're writing the JSON yourself.
More ideal solution whould be to have Jackson convert, but have some kind of post-conversion-call to add additional XX_condtion properties to the object. Maybe start by overriding the default object-serializer (if possible)?
I wanted to know the best practice of how to validate the ID of the path of my Rest API.
For example:
When I do a GET to retrieve a Building, I need to validate first if the {universityId} and {campusId} are actually valid (Existing in the DB) before proceeding.
Right now I have implemented a custom RepositoryValidation that provides those functionalities by throwing a ResourceNotFoundException() and those methods are called in my service class for the GET,PUT,POST..etc
Is there a better way to do the validation? I have read about Interceptors or Filters but not sure if that's the best practice.
Custom Exception:
#ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException() {
super();
}
public ResourceNotFoundException(String message) {
super(message);
}
Repository Validation:
#Component
public class RepositoryValidation {
#Autowired
private UniversityRepository universityRepository;
#Autowired
private CampusRepository campusRepository;
#Autowired
private BuildingRepository buildingRepository;
public void checkIfUniversityExists(Long universityId){
if (!universityRepository.exists(universityId))
throw new ResourceNotFoundException("University with id: " + universityId + " not found");
}
public void checkIfCampusExists(Long campusId){
if (!campusRepository.exists(campusId))
throw new ResourceNotFoundException("Campus with id: " + campusId + " not found");
}
public void checkIfBuildingExists(Long buildingId){
if (!buildingRepository.exists(buildingId))
throw new ResourceNotFoundException("Building with id: " + buildingId + " not found");
}
}
Service:
#Service
public class BuildingService {
#Autowired
private BuildingRepository buildingRepository;
#Autowired
private RepositoryValidation repositoryValidation;
public Iterable<Building> list(Long campusId) {
return buildingRepository.findAllByCampusId(campusId);
}
#Transactional
public Building create(Building building) {
return buildingRepository.save(building);
}
public Building read(Long buildingId,Long campusId) {
repositoryValidation.checkIfCampusExists(campusId);
repositoryValidation.checkIfBuildingExists(buildingId);
return buildingRepository.findBuildingByIdAndCampusId(buildingId,campusId);
}
#Transactional
public Building update(Long buildingId,Building update) {
repositoryValidation.checkIfBuildingExists(buildingId);
Building building = buildingRepository.findOne(buildingId);
building.setBuildingName(update.getBuildingName());
return buildingRepository.save(building);
}
#Transactional
public void delete(Long buildingId,Long campusId) {
repositoryValidation.checkIfCampusExists(campusId);
repositoryValidation.checkIfBuildingExists(buildingId);
buildingRepository.deleteBuildingByIdAndCampusId(buildingId, campusId);
}
You should look into Springs' Validation-Beanvalidation.
With this, you can use #Valid to do simple validations on properties, for example:
#NotNull
#Size(max=64)
private String name;
You can also add the #Valid to inputs in a REST endpoint:
#RequestMapping("/foo", method=RequestMethod.POST)
public void processFoo(#Valid Foo foo) { /* ... */ }
For your needs, you could consider creating a custom #Constraint.
You would first create the constraint annotation:
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy=MyConstraintValidator.class)
public #interface MyConstraint {
}
And then the constraint validator:
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
#Autowired;
private Foo aDependency;
...
}
Notice you can inject other Spring beans into the ConstraintValidator as well.
Once implemented, this could easily be re-used and looks very concise.
Feign default expander to convert param:
final class ToStringExpander implements Expander {
#Override
public String expand(Object value) {
return value.toString();
}
}
I want custom it to convert user to support GET param, like this
#FeignClient("xx")
interface UserService{
#RequestMapping(value="/users",method=GET)
public List<User> findBy(#ModelAttribute User user);
}
userService.findBy(user);
What can i do?
First,you must write a expander like ToJsonExpander:
public class ToJsonExpander implements Param.Expander {
private static ObjectMapper objectMapper = new ObjectMapper();
public String expand(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new ExpanderException(e);
}
}
}
Second, write a AnnotatedParameterProcessor like JsonArgumentParameterProcessor to add expander for your processor.
public class JsonArgumentParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<JsonArgument> ANNOTATION = JsonArgument.class;
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
MethodMetadata data = context.getMethodMetadata();
String name = ANNOTATION.cast(annotation).value();
String method = data.template().method();
Util.checkState(Util.emptyToNull(name) != null,
"JsonArgument.value() was empty on parameter %s", context.getParameterIndex());
context.setParameterName(name);
if (method != null && (HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.DELETE.matches(method))) {
data.formParams().add(name);
} else {
`data.indexToExpanderClass().put(context.getParameterIndex(), ToJsonExpander.class);`
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
data.template().query(name, query);
}
return true;
}
}
Third,add it to Feign configuration.
#Bean
public Contract feignContract(){
List<AnnotatedParameterProcessor> processors = new ArrayList<>();
processors.add(new JsonArgumentParameterProcessor());
processors.add(new PathVariableParameterProcessor());
processors.add(new RequestHeaderParameterProcessor());
processors.add(new RequestParamParameterProcessor());
return new SpringMvcContract(processors);
}
Now, you can use #JsonArgument to send model argument like:
public void saveV10(#JsonArgument("session") Session session);
I don't know what #ModelAttribute does but I was looking for a way to convert #RequestParam values so I did this:
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar;
import org.springframework.format.FormatterRegistry;
import org.springframework.stereotype.Component;
import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.E164;
#Component
public class PhoneNumberFeignFormatterRegistrar implements FeignFormatterRegistrar {
private final PhoneNumberUtil phoneNumberUtil;
public PhoneNumberFeignFormatterRegistrar(PhoneNumberUtil phoneNumberUtil) {
this.phoneNumberUtil = phoneNumberUtil;
}
#Override
public void registerFormatters(FormatterRegistry registry) {
registry.addConverter(Phonenumber.PhoneNumber.class, String.class, source -> phoneNumberUtil.format(source, E164));
}
}
Now stuff like the following works
import com.google.i18n.phonenumbers.Phonenumber;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
#FeignClient("data-service")
public interface DataClient {
#RequestMapping(method = RequestMethod.GET, value = "/phoneNumbers/search/findByPhoneNumber")
Resource<PhoneNumberRecord> getPhoneNumber(#RequestParam("phoneNumber") Phonenumber.PhoneNumber phoneNumber);
}
As the open feign issue and spring doc say:
The OpenFeign #QueryMap annotation provides support for POJOs to be used as GET parameter maps.
Spring Cloud OpenFeign provides an equivalent #SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map since 2.1.0.
You can use it like this:
#GetMapping("user")
String getUser(#SpringQueryMap User user);
public class User {
private String name;
private int age;
...
}
I have an entity named EmployeeDepartment as below
#IdClass(EmployeeDepartmentPK.class) //EmployeeDepartmentPK is a serializeable object
#Entity
EmployeeDepartment{
#Id
private String employeeID;
#Id
private String departmentCode;
---- Getters, Setters and other props/columns
}
and I have a Spring Data Repository defined as as below
#RepositoryRestResource(....)
public interface IEmployeeDepartmentRepository extends PagingAndSortingRepository<EmployeeDepartment, EmployeeDepartmentPK> {
}
Further, I have a converter registered to convert from String to EmployeeDepartmentPK.
Now, for an entity, qualified by ID employeeID="abc123" and departmentCode="JBG", I expect the ID to use when SDR interface is called is abc123_JBG.
For example http://localhost/EmployeeDepartment/abc123_JBG should fetch me the result and indeed it does.
But, when I try to save an entity using PUT, the ID property available in BasicPersistentEntity class of Spring Data Commons is having a value of
abc123_JBG for departmentCode. This is wrong. I'm not sure if this is an expected behaviour.
Please help.
Thanks!
Currently Spring Data REST only supports compound keys that are represented as by a single field. That effectively means only #EmbeddedId is supported. I've filed DATAJPA-770 to fix that.
If you can switch to #EmbeddedId you still need to teach Spring Data REST the way you'd like to represent your complex identifier in the URI and how to transform the path segment back into an instance of your id type. To achieve that, implement a BackendIdConverter and register it as Spring bean.
#Component
class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
// Make sure you validate the input
String[] parts = id.split("_");
return new YourEmbeddedIdType(parts[0], parts[1]);
}
#Override
public String toRequestId(Serializable source, Class<?> entityType) {
YourIdType id = (YourIdType) source;
return String.format("%s_%s", …);
}
#Override
public boolean supports(Class<?> type) {
return YourDomainType.class.equals(type);
}
}
If you can't use #EmbeddedId, you can still use #IdClass. For that, you need the BackendIdConverter as Oliver Gierke answered, but you also need to add a Lookup for your domain type:
#Configuration
public class IdClassAllowingConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.withEntityLookup().forRepository(EmployeeDepartmentRepository.class, (EmployeeDepartment ed) -> {
EmployeeDepartmentPK pk = new EmployeeDepartmentPK();
pk.setDepartmentId(ed.getDepartmentId());
pk.setEmployeeId(ed.getEmployeeId());
return pk;
}, EmployeeDepartmentRepository::findOne);
}
}
Use #BasePathAwareController to customize Spring data rest controller.
#BasePathAwareController
public class CustInfoCustAcctController {
#Autowired
CustInfoCustAcctRepository cicaRepo;
#RequestMapping(value = "/custInfoCustAccts/{id}", method = RequestMethod.GET)
public #ResponseBody custInfoCustAccts getOne(#PathVariable("id") String id) {
String[] parts = id.split("_");
CustInfoCustAcctKey key = new CustInfoCustAcctKey(parts[0],parts[1]);
return cicaRepo.getOne(key);
}
}
It's work fine for me with sample uri /api/custInfoCustAccts/89232_70
A more generic approach would be following -
package com.pratham.persistence.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Customization of how composite ids are exposed in URIs.
* The implementation will convert the Ids marked with {#link EmbeddedId} to base64 encoded json
* in order to expose them properly within URI.
*
* #author im-pratham
*/
#Component
#RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
private final ObjectMapper objectMapper;
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return getFieldWithEmbeddedAnnotation(entityType)
.map(Field::getType)
.map(ret -> {
try {
String decodedId = new String(Base64.getUrlDecoder().decode(id));
return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
} catch (JsonProcessingException ignored) {
return null;
}
})
.orElse(id);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
try {
String json = objectMapper.writeValueAsString(id);
return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
} catch (JsonProcessingException ignored) {
return id.toString();
}
}
#Override
public boolean supports(#NonNull Class<?> entity) {
return isEmbeddedIdAnnotationPresent(entity);
}
private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
return getFieldWithEmbeddedAnnotation(entity)
.isPresent();
}
#NotNull
private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
return Arrays.stream(entity.getDeclaredFields())
.filter(method -> method.isAnnotationPresent(EmbeddedId.class))
.findFirst();
}
}
I am trying to set up a translation service which will use gnu gettext. The basic idea is from: https://thedarkgod.wordpress.com/2009/01/18/java-webapp-localization-through-gettext/
But I would like it to be implemented as a service. For some odd reasons, I would like to have this class:
import webapp.service.TranslationService;
import org.springframework.context.i18n.LocaleContextHolder;
/**
* AppStrings<p>
* <p/>
* DOC-TODO:
*/
#Service("applicationStrings")
public class ApplicationStrings {
#Autowired private TranslationService translationService;
public String CART_SUBTYPE = "Cart";
public ApplicationStrings(){
Locale locale = LocaleContextHolder.getLocale();
//translationService.initLocale();
this.updateLocale();
}
public void updateLocale(){
Locale locale = LocaleContextHolder.getLocale();
translationService.updateLocale(locale);
this.setLocale(locale);
}
public void setLocale(Locale locale){
//this.CART_SUBTYPE = translationService._("Cart");
this.CART_SUBTYPE = "CART_DEF - Check ApplicationStrings";
}
}
Part of the code is commented as it doesn't really work... but it might reveal my target. The indeed problematic service class looks like this:
package webapp.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import gnu.gettext.GettextResource;
import java.util.ResourceBundle;
import java.util.Locale;
import java.text.MessageFormat;
import java.util.Hashtable;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
/**
* From: https://thedarkgod.wordpress.com/2009/01/18/java-webapp-localization-through-gettext/
*
*/
#Service("translationService")
public class TranslationService {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static Hashtable<Locale, ResourceBundle> trht = new Hashtable<Locale, ResourceBundle> ();
private ResourceBundle myResources = null;
public TranslationService () {
Locale locale = LocaleContextHolder.getLocale();
if ( locale == null){
logger.warn("Setting a default locale!");
locale = Locale.ENGLISH;
}
this.updateLocale(locale);
}
public TranslationService (Locale locale) {
this.updateLocale(locale);
}
public void initLocale () {
Locale locale = LocaleContextHolder.getLocale();
this.updateLocale(locale);
}
public void updateLocale(Locale locale)
{
synchronized (trht)
{
if (!trht.contains (locale))
{
try
{
myResources = GettextResource.getBundle("translation", locale);
}
catch (Exception e)
{
logger.error("UPDATE: Exception.");
e.printStackTrace();
}
trht.put ((Locale) locale.clone(), myResources);
}
else
myResources = trht.get (locale);
}
}
public String _(String s)
{
if (myResources == null) return s;
return GettextResource.gettext (myResources, s);
}
public String N_(String singular, String plural, long n)
{
if (myResources == null) return (n == 1 ? singular : plural);
return GettextResource.ngettext (myResources, singular,
plural, n);
}
public String format (String s, Object ... args)
{
return MessageFormat.format (_(s), args);
}
public String formatN (String singular, String plural,
long n, Object ... args)
{
return MessageFormat.format (N_(singular, plural, n), args);
}
}
These classes are not even a bit used in my application, but of course spring will instantiate them and the error looks like this:
java.lang.NullPointerException
at webapp.constant.ApplicationStrings.updateLocale(ApplicationStrings.java:34)
at webapp.constant.ApplicationStrings.<init>(ApplicationStrings.java:29)
Please note that all other #service are working so I assume it is not a problem of some xml configuration, for which I found few questions (and answers) on stackoverflow, again other services are working so the configuration should be fine.
I assume it is my newbie approach which might miss some simple keyword.. or even concept..
Thanks and cheers
Together with the answers, as this might be a general approach to gettext and indeed related to the question, I would appreciate a couple of comments on the actual approach.
Also I am totally not sure about the "sychronized" part: can this be the problem?
You are calling dependencies in your constructor. Spring wires the bean dependencies after constructing the object so here you are trying to call those methods a bit too early, Spring had no chance to wire your bean at that moment. To solve the issue you could make use of the #PostConstruct annotation. A method marked with this is always called by Spring after constructing and wiring up a bean.
e.g.
public ApplicationStrings() {
}
#PostConstruct
public void init() {
//translationService.initLocale();
this.updateLocale();
}