package fr.openwide.core.spring.property.service;
import static fr.openwide.core.spring.property.SpringPropertyIds.AVAILABLE_LOCALES;
import static fr.openwide.core.spring.property.SpringPropertyIds.DEFAULT_LOCALE;
import java.io.File;
import java.io.FileFilter;
import java.math.BigDecimal;
import java.net.URI;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.javatuples.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.base.Converter;
import com.google.common.base.Enums;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import fr.openwide.core.commons.util.functional.converter.StringBigDecimalConverter;
import fr.openwide.core.commons.util.functional.converter.StringBooleanConverter;
import fr.openwide.core.commons.util.functional.converter.StringDateConverter;
import fr.openwide.core.commons.util.functional.converter.StringDateTimeConverter;
import fr.openwide.core.commons.util.functional.converter.StringDirectoryFileCreatingConverter;
import fr.openwide.core.commons.util.functional.converter.StringFileConverter;
import fr.openwide.core.commons.util.functional.converter.StringLocaleConverter;
import fr.openwide.core.commons.util.functional.converter.StringTimeConverter;
import fr.openwide.core.commons.util.functional.converter.StringURIConverter;
import fr.openwide.core.jpa.exception.SecurityServiceException;
import fr.openwide.core.jpa.exception.ServiceException;
import fr.openwide.core.spring.config.spring.event.PropertyRegistryInitEvent;
import fr.openwide.core.spring.property.SpringPropertyIds;
import fr.openwide.core.spring.property.dao.IImmutablePropertyDao;
import fr.openwide.core.spring.property.dao.IMutablePropertyDao;
import fr.openwide.core.spring.property.exception.PropertyServiceDuplicateRegistrationException;
import fr.openwide.core.spring.property.exception.PropertyServiceIncompleteRegistrationException;
import fr.openwide.core.spring.property.model.IImmutablePropertyRegistryKey;
import fr.openwide.core.spring.property.model.IMutablePropertyRegistryKey;
import fr.openwide.core.spring.property.model.IMutablePropertyValueMap;
import fr.openwide.core.spring.property.model.IPropertyRegistryKey;
import fr.openwide.core.spring.property.model.IPropertyRegistryKeyDeclaration;
import fr.openwide.core.spring.property.model.ImmutablePropertyId;
import fr.openwide.core.spring.property.model.MutablePropertyId;
import fr.openwide.core.spring.property.model.PropertyId;
import fr.openwide.core.spring.property.model.PropertyIdTemplate;
/**
* Use this service to retrieve registered application properties.
* It handles both mutable and immutable properties ; immutable properties are retrieved from properties resources files
* ({@link IImmutablePropertyDao}) and mutable properties are stored in database ({@link IMutablePropertyDao}).
* @see {@link IPropertyRegistry} to register application properties.
*/
public class PropertyServiceImpl implements IConfigurablePropertyService, ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(PropertyServiceImpl.class);
private final Map<IPropertyRegistryKey<?>, Pair<? extends Converter<String, ?>, ? extends Supplier<?>>> propertyInformationMap =
Maps.newLinkedHashMap();
@Autowired
@Lazy // Mutable properties may require a more complex infrastructure, whose setup may require access to immutable properties
private IMutablePropertyDao mutablePropertyDao;
@Autowired
private IImmutablePropertyDao immutablePropertyDao;
private ApplicationEventPublisher applicationEventPublisher;
private TransactionTemplate writeTransactionTemplate;
@Autowired
@Lazy // Mutable properties may require a more complex infrastructure, whose setup may require access to immutable properties
public void setPlatformTransactionManager(PlatformTransactionManager transactionManager) {
DefaultTransactionAttribute writeTransactionAttribute =
new DefaultTransactionAttribute(TransactionAttribute.PROPAGATION_REQUIRED);
writeTransactionAttribute.setReadOnly(false);
writeTransactionTemplate = new TransactionTemplate(transactionManager, writeTransactionAttribute);
}
@PostConstruct
public void init() throws PropertyServiceIncompleteRegistrationException {
applicationEventPublisher.publishEvent(new PropertyRegistryInitEvent(this));
checkNoIncompleteRegistration();
}
private void checkNoIncompleteRegistration() throws PropertyServiceIncompleteRegistrationException {
SetMultimap<IPropertyRegistryKeyDeclaration, IPropertyRegistryKey<?>> declarationsToRegisteredKeys = LinkedHashMultimap.create();
for (IPropertyRegistryKey<?> key : propertyInformationMap.keySet()) {
declarationsToRegisteredKeys.put(key.getDeclaration(), key);
}
SetMultimap<IPropertyRegistryKeyDeclaration, IPropertyRegistryKey<?>> declarationsToUnregisteredKeys = LinkedHashMultimap.create();
for (Map.Entry<IPropertyRegistryKeyDeclaration, Set<IPropertyRegistryKey<?>>> declarationAndRegisteredKeys
: Multimaps.asMap(declarationsToRegisteredKeys).entrySet()) {
IPropertyRegistryKeyDeclaration declaration = declarationAndRegisteredKeys.getKey();
Set<IPropertyRegistryKey<?>> registeredKeys = declarationAndRegisteredKeys.getValue();
declarationsToUnregisteredKeys.putAll(
declaration,
Sets.difference(declaration.getDeclaredKeys(), registeredKeys)
);
}
if (!declarationsToUnregisteredKeys.isEmpty()) {
throw new PropertyServiceIncompleteRegistrationException(
String.format(
"The registration of property keys is incomplete."
+ " Here are the missing keys for each declaration: %s",
declarationsToUnregisteredKeys
)
);
}
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public <T> void register(IMutablePropertyRegistryKey<T> propertyId, Converter<String, T> converter) {
register(propertyId, converter, (T) null);
}
@Override
public <T> void register(IMutablePropertyRegistryKey<T> propertyId, Converter<String, T> converter, T defaultValue) {
register(propertyId, converter, Suppliers.ofInstance(defaultValue));
}
@Override
public <T> void register(IMutablePropertyRegistryKey<T> propertyId, Converter<String, T> converter, Supplier<? extends T> defaultValueSupplier) {
registerProperty(propertyId, converter, defaultValueSupplier);
}
@Override
public <T> void register(IImmutablePropertyRegistryKey<T> propertyId, Function<String, ? extends T> function) {
register(propertyId, function, (T) null);
}
@Override
public <T> void register(IImmutablePropertyRegistryKey<T> propertyId, Function<String, ? extends T> function, T defaultValue) {
register(propertyId, function, Suppliers.ofInstance(defaultValue));
}
@Override
public <T> void register(IImmutablePropertyRegistryKey<T> propertyId, final Function<String, ? extends T> function, Supplier<? extends T> defaultValueSupplier) {
registerProperty(propertyId, new Converter<String, T>() {
@Override
protected T doForward(String a) {
return function.apply(a);
}
@Override
protected String doBackward(T b) {
throw new IllegalStateException("Unable to update immutable property.");
}
/**
* Workaround sonar/findbugs - https://github.com/google/guava/issues/1858
* Guava Converter overrides only equals to add javadoc, but findbugs warns about non coherent equals/hashcode
* possible issue.
*/
@Override
public boolean equals(Object object) {
return super.equals(object);
}
/**
* Workaround sonar/findbugs - see #equals(Object)
*/
@Override
public int hashCode() {
return super.hashCode();
}
}, defaultValueSupplier);
}
protected <T> void registerProperty(IPropertyRegistryKey<T> propertyId, Converter<String, ? extends T> converter) {
registerProperty(propertyId, converter, (T) null);
}
protected <T> void registerProperty(IPropertyRegistryKey<T> propertyId, Converter<String, ? extends T> converter, T defaultValue) {
registerProperty(propertyId, converter, Suppliers.ofInstance(defaultValue));
}
protected <T> void registerProperty(IPropertyRegistryKey<T> propertyId, Converter<String, ? extends T> converter, Supplier<? extends T> defaultValueSupplier) {
Preconditions.checkNotNull(propertyId);
Preconditions.checkNotNull(converter);
Preconditions.checkNotNull(defaultValueSupplier);
if (propertyInformationMap.containsKey(propertyId)) {
throw new PropertyServiceDuplicateRegistrationException(String.format(
"Property '%1s' has already been registered."
+ " There might be multiple property IDs in your application that use the same key.",
propertyId
));
}
propertyInformationMap.put(propertyId, Pair.with(converter, defaultValueSupplier));
}
@Override
public void registerString(IPropertyRegistryKey<String> propertyId) {
registerString(propertyId, null);
}
@Override
public void registerString(IPropertyRegistryKey<String> propertyId, String defaultValue) {
registerProperty(propertyId, Converter.<String>identity(), defaultValue);
}
@Override
public void registerLong(IPropertyRegistryKey<Long> propertyId) {
registerLong(propertyId, null);
}
@Override
public void registerLong(IPropertyRegistryKey<Long> propertyId, Long defaultValue) {
registerProperty(propertyId, Longs.stringConverter(), defaultValue);
}
@Override
public void registerInteger(IPropertyRegistryKey<Integer> propertyId) {
registerInteger(propertyId, null);
}
@Override
public void registerInteger(IPropertyRegistryKey<Integer> propertyId, Integer defaultValue) {
registerProperty(propertyId, Ints.stringConverter(), defaultValue);
}
@Override
public void registerFloat(IPropertyRegistryKey<Float> propertyId) {
registerFloat(propertyId, null);
}
@Override
public void registerFloat(IPropertyRegistryKey<Float> propertyId, Float defaultValue) {
registerProperty(propertyId, Floats.stringConverter(), defaultValue);
}
@Override
public void registerDouble(IPropertyRegistryKey<Double> propertyId) {
registerDouble(propertyId, null);
}
@Override
public void registerDouble(IPropertyRegistryKey<Double> propertyId, Double defaultValue) {
registerProperty(propertyId, Doubles.stringConverter(), defaultValue);
}
@Override
public void registerBigDecimal(IPropertyRegistryKey<BigDecimal> propertyId) {
registerBigDecimal(propertyId, null);
}
@Override
public void registerBigDecimal(IPropertyRegistryKey<BigDecimal> propertyId, BigDecimal defaultValue) {
registerProperty(propertyId, StringBigDecimalConverter.get(), defaultValue);
}
@Override
public void registerBoolean(IPropertyRegistryKey<Boolean> propertyId) {
registerBoolean(propertyId, null);
}
@Override
public void registerBoolean(IPropertyRegistryKey<Boolean> propertyId, Boolean defaultValue) {
registerProperty(propertyId, StringBooleanConverter.get(), defaultValue);
}
@Override
public <E extends Enum<E>> void registerEnum(IPropertyRegistryKey<E> propertyId, Class<E> clazz) {
registerEnum(propertyId, clazz, null);
}
@Override
public <E extends Enum<E>> void registerEnum(IPropertyRegistryKey<E> propertyId, Class<E> clazz, E defaultValue) {
registerProperty(propertyId, Enums.stringConverter(clazz), defaultValue);
}
@Override
public void registerDate(IPropertyRegistryKey<Date> propertyId) {
registerDate(propertyId, (Date) null);
}
@Override
public void registerDate(IPropertyRegistryKey<Date> propertyId, String defaultValue) {
registerDate(propertyId, StringDateConverter.get().convert(defaultValue));
}
@Override
public void registerDate(IPropertyRegistryKey<Date> propertyId, Date defaultValue) {
registerProperty(propertyId, StringDateConverter.get(), defaultValue);
}
@Override
public void registerTime(IPropertyRegistryKey<Date> propertyId) {
registerTime(propertyId, (Date) null);
}
@Override
public void registerTime(IPropertyRegistryKey<Date> propertyId, String defaultValue) {
registerTime(propertyId, StringTimeConverter.get().convert(defaultValue));
}
@Override
public void registerTime(IPropertyRegistryKey<Date> propertyId, Date defaultValue) {
registerProperty(propertyId, StringTimeConverter.get(), defaultValue);
}
@Override
public void registerDateTime(IPropertyRegistryKey<Date> propertyId) {
registerDateTime(propertyId, null);
}
@Override
public void registerDateTime(IPropertyRegistryKey<Date> propertyId, Date defaultValue) {
registerProperty(propertyId, StringDateTimeConverter.get(), defaultValue);
}
@Override
public void registerLocale(IPropertyRegistryKey<Locale> propertyId) {
registerLocale(propertyId, null);
}
@Override
public void registerLocale(IPropertyRegistryKey<Locale> propertyId, Locale defaultValue) {
registerProperty(propertyId, StringLocaleConverter.get(), defaultValue);
}
@Override
public void registerFile(IPropertyRegistryKey<File> propertyId, FileFilter filter) {
registerFile(propertyId, filter, null);
}
@Override
public void registerFile(IPropertyRegistryKey<File> propertyId, final FileFilter filter, final File defaultValue) {
Preconditions.checkNotNull(filter);
registerProperty(propertyId, new StringFileConverter(filter), new Supplier<File>() {
@Override
public File get() {
// Make this check *only* if we actually use the default value.
Preconditions.checkState(
defaultValue == null || filter.accept(defaultValue),
"The default value " + defaultValue + " does not match the given file filter " + filter
);
return defaultValue;
}
});
}
@Override
public void registerWriteableDirectoryFile(IPropertyRegistryKey<File> propertyId) {
registerWriteableDirectoryFile(propertyId, null);
}
@Override
public void registerWriteableDirectoryFile(IPropertyRegistryKey<File> propertyId, File defaultValue) {
registerProperty(propertyId, StringDirectoryFileCreatingConverter.get(), defaultValue);
}
@Override
public void registerURI(IPropertyRegistryKey<URI> propertyId) {
registerURI(propertyId, null);
}
@Override
public void registerURI(IPropertyRegistryKey<URI> propertyId, URI defaultValue) {
registerProperty(propertyId, StringURIConverter.get(), defaultValue);
}
@Override
public <T> T get(final PropertyId<T> propertyId) {
Preconditions.checkNotNull(propertyId);
Pair<Converter<String, T>, Supplier<T>> information = getRegistrationInformation(propertyId);
T value = information.getValue0().convert(getAsStringUnsafe(propertyId));
if (value == null) {
T defaultValue = information.getValue1().get();
if (defaultValue != null) {
value = defaultValue;
LOGGER.debug(String.format("Property '%1s' has no value, fallback on default value.", propertyId));
} else {
LOGGER.info(String.format("Property '%1s' has no value and default value is undefined.", propertyId));
}
}
return value;
}
@Override
public <T> String getAsString(final PropertyId<T> propertyId) {
getRegistrationInformation(propertyId); // Just check that this property was actually registered
return getAsStringUnsafe(propertyId);
}
private String getAsStringUnsafe(PropertyId<?> propertyId) {
String valueAsString = null;
if (propertyId instanceof ImmutablePropertyId) {
valueAsString = immutablePropertyDao.get(propertyId.getKey());
} else if (propertyId instanceof MutablePropertyId) {
valueAsString = mutablePropertyDao.getInTransaction(propertyId.getKey());
} else {
throw new IllegalStateException(String.format("Unknown type of property : '%1s'.", propertyId));
}
return valueAsString;
}
@Override
public <T> void set(final MutablePropertyId<T> propertyId, final T value) throws ServiceException, SecurityServiceException {
Preconditions.checkNotNull(propertyId);
Pair<Converter<String, T>, Supplier<T>> information = getRegistrationInformation(propertyId);
mutablePropertyDao.setInTransaction(propertyId.getKey(), information.getValue0().reverse().convert(value));
}
private <T> void set(IMutablePropertyValueMap.Entry<T> idToValueEntry) throws ServiceException, SecurityServiceException {
set(idToValueEntry.getKey(), idToValueEntry.getValue());
}
@Override
public void setAll(final IMutablePropertyValueMap valueMap) throws ServiceException, SecurityServiceException {
Objects.requireNonNull(valueMap);
writeTransactionTemplate.execute(
new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
for (IMutablePropertyValueMap.Entry<?> idToValueEntry : valueMap) {
set(idToValueEntry);
}
} catch (RuntimeException | ServiceException | SecurityServiceException e) {
throw new IllegalStateException(String.format("Error while updating properties"), e);
}
}
}
);
}
private <T> Pair<Converter<String, T>, Supplier<T>> getRegistrationInformation(PropertyId<T> propertyId) {
PropertyIdTemplate<T, ?> template = propertyId.getTemplate();
@SuppressWarnings("unchecked")
Pair<Converter<String, T>, Supplier<T>> information = (Pair<Converter<String, T>, Supplier<T>>)
propertyInformationMap.get(template != null ? template : propertyId);
if (information == null || information.getValue0() == null) {
throw new PropertyServiceIncompleteRegistrationException(
String.format("The following property was not properly registered: %1s", propertyId)
);
}
return information;
}
@Override
public List<IPropertyRegistryKey<?>> listRegistered() {
return Lists.newArrayList(propertyInformationMap.keySet());
}
// TODO PropertyService : remove this method from service
@Override
public boolean isConfigurationTypeDevelopment() {
return SpringPropertyIds.CONFIGURATION_TYPE_DEVELOPMENT.equals(get(SpringPropertyIds.CONFIGURATION_TYPE));
}
// TODO PropertyService : remove this method from service
/**
* <p> Le but est de partir d'une locale
* quelconque et d'aboutir obligatoirement à une locale provenant de la liste
* <i>locale.availableLocales</i>.</p>
*
* <p>Le mapping se fait ainsi :
* <ul>
* <li>si la locale est dans locale.availableLocales, alors on utilise la locale</li>
* <li>sinon on vérifié si le <i>Language</i> de la locale correspond à un <i>Language</i>
* dans locale.availableLocales ; alors on utilise la locale correspondante
* </li>
* <li>sinon on utilise <i>locale.default</i></li>
* </ul>
* </p>
*
* <p>Exemple :<br/>
* <code>locale.availableLocales=fr en</code><br/>
* <code>locale.default=fr</code><br/>
* <br/>
* Les résultats seront les suivants
* <ul>
* <li>fr -> fr (correspondance exacte)</li>
* <li>fr_FR -> fr (correspondance sur Language)</li>
* <li>en -> en (correspondance exacte)</li>
* <li>en_US -> en (correspondance sur Language)</li>
* <li>ar_SA -> fr (défaut)</li>
* </ul>
* </p>
*
* @param locale
* @return locale, not null, from locale.availableLocales
*/
@Override
public Locale toAvailableLocale(Locale locale) {
if (locale != null) {
Set<Locale> availableLocales = get(AVAILABLE_LOCALES);
if (availableLocales.contains(locale)) {
return locale;
} else {
for (Locale availableLocale : availableLocales) {
if (availableLocale.getLanguage().equals(locale.getLanguage())) {
return availableLocale;
}
}
}
}
// default locale from configuration
return get(DEFAULT_LOCALE);
}
}