blob: 1cb5fef64f05bcbf51d76c7539d612dd3977f755 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.tamaya.inject.spi;
import org.apache.tamaya.ConfigException;
import org.apache.tamaya.Configuration;
import org.apache.tamaya.TypeLiteral;
import org.apache.tamaya.inject.api.DynamicValue;
import org.apache.tamaya.inject.api.UpdatePolicy;
import org.apache.tamaya.spi.PropertyConverter;
import org.apache.tamaya.spi.ConversionContext;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Basic abstract implementation skeleton for a {@link DynamicValue}. This can be used to support values that may
* change during runtime. Hereby external code (could be Tamaya configuration listners or client
* code), can apply a new value. Depending on the {@link org.apache.tamaya.inject.api.UpdatePolicy} the new value is applied immedeately, when the
* change has been identified, or it requires an programmatic commit by client code to
* activate the change in the {@link DynamicValue}. Similarly an instance also can ignore all
* later changes to the value.
*
* <h1>Implementation Specification</h1>
* This class is
* <ul>
* <li>Serializable, when also the item stored is serializable</li>
* <li>Thread safe</li>
* </ul>
*
* @param <T> The type of the value.
*/
public abstract class BaseDynamicValue<T> implements DynamicValue<T> {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(DynamicValue.class.getName());
/** The value owner used for PropertyChangeEvents. */
private Object owner;
/**
* The property name of the entry.
*/
private String propertyName;
private Configuration configuration;
/**
* Policy that defines how new values are applied, be default it is applied initially once, but never updated
* anymore.
*/
private UpdatePolicy updatePolicy = UpdatePolicy.NEVER;
/** The targe type. */
private TypeLiteral<T> targetType;
/**
* The current value, never null.
*/
protected transient T value;
/** The last discarded value. */
protected transient T discarded;
/** Any new value, not yet applied. */
protected transient T newValue;
/** The configured default value, before type conversion. */
private String defaultValue;
/** The createList of candidate keys to be used. */
private List<String> keys = new ArrayList<>();
/** The registered listeners. */
private final WeakList<PropertyChangeListener> listeners = new WeakList<>();
/**
* Creates a new instance.
* @param owner the owner, not null.
* @param propertyName the property name, not null.
* @param targetType the target type.
* @param keys the candidate keys.
* @param configuration the configuration, not null.
*/
public BaseDynamicValue(Object owner, String propertyName, TypeLiteral targetType, List<String> keys,
Configuration configuration){
if(keys == null || keys.isEmpty()){
throw new ConfigException("At least one key is required.");
}
this.owner = owner;
this.configuration = Objects.requireNonNull(configuration);
this.propertyName = Objects.requireNonNull(propertyName);
this.targetType = Objects.requireNonNull(targetType);
this.keys.addAll(keys);
}
/**
* Get the default value, used if no value could be evaluated.
* @return the default value, or null.
*/
public String getDefaultValue() {
return defaultValue;
}
/**
* Set the default value to be used.
* @param defaultValue the default value.
*/
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
/**
* Get the configuration to evaluate.
* @return the configuration, never null.
*/
protected Configuration getConfiguration(){
return configuration;
}
/**
* Get the corresponding property name.
* @return the property name.
*/
protected String getPropertyName(){
return propertyName;
}
/**
* Get the owner of this dynamic value instance.
* @return the owner, never null.
*/
protected Object getOwner(){
return owner;
}
/**
* Get the targeted keys, in evaluation order.
* @return the keys evaluated.
*/
public List<String> getKeys(){
return Collections.unmodifiableList(keys);
}
/**
* Get the target type.
* @return the target type, not null.
*/
public TypeLiteral<T> getTargetType(){
return targetType;
}
@Override
public void commit() {
if(!Objects.equals(newValue, value)) {
T oldValue = this.value;
value = newValue;
discarded = null;
publishChangeEvent(this.value, newValue);
newValue = null;
}
}
@Override
public void discard() {
if(newValue!=null){
discarded = newValue;
}
newValue = null;
}
@Override
public UpdatePolicy getUpdatePolicy() {
return updatePolicy;
}
@Override
public void addListener(PropertyChangeListener l) {
synchronized(listeners){
if(!listeners.contains(l)){
listeners.add(l);
}
}
}
@Override
public void removeListener(PropertyChangeListener l) {
synchronized(listeners){
listeners.remove(l);
}
}
@Override
public T get() {
updateValue();
return value;
}
@Override
public boolean updateValue() {
Configuration config = getConfiguration();
T val = evaluateValue();
if(value == null){
value = val;
return true;
}else if(discarded!=null && discarded.equals(val)){
// the evaluated value has been discarded and will be flagged out.
return false;
}else{
// Reset discarded state for a new value.
discarded = null;
}
if(!Objects.equals(val, value)){
switch (updatePolicy){
case EXPLICIT:
newValue = val;
break;
case IMMEDIATE:
this.value = val;
publishChangeEvent(this.value, val);
break;
case LOG_ONLY:
LOG.info("New config value for keys " + keys + " detected, but not yet applied.");
break;
case NEVER:
default:
LOG.finest("New config value for keys " + keys + " detected, but ignored.");
break;
}
return true;
}
return false;
}
/**
* Publishes a change event to all listeners.
* @param newValue the new value
* @param oldValue the new old value
*/
protected void publishChangeEvent(T oldValue, T newValue) {
PropertyChangeEvent evt = new PropertyChangeEvent(getOwner(), getPropertyName(),oldValue, newValue);
synchronized (listeners){
listeners.forEach(l -> {
try{
l.propertyChange(evt);
}catch(Exception e){
LOG.log(Level.SEVERE, "Error in config change listener: " + l, e);
}
});
}
}
/**
* Allows to customize type conversion if needed, e.g. based on some annotations defined.
* @return the custom converter, which replaces the default converters, ot null.
*/
protected PropertyConverter<T> getCustomConverter(){
return null;
}
@Override
public T evaluateValue() {
T value = null;
List<PropertyConverter<T>> converters = new ArrayList<>();
if (this.getCustomConverter() != null) {
converters.add(this.getCustomConverter());
}
converters.addAll(getConfiguration().getContext().getPropertyConverters(targetType));
for (String key : keys) {
ConversionContext ctx = new ConversionContext.Builder(key, targetType).build();
String stringVal = getConfiguration().getOrDefault(key, String.class, null);
if(stringVal!=null) {
if(String.class.equals(targetType.getType())){
value = (T)stringVal;
}
for(PropertyConverter<T> conv:converters){
try{
value = conv.convert(stringVal, ctx);
if(value!=null){
break;
}
}catch(Exception e){
LOG.warning("failed to convert: " + ctx);
}
}
}
}
if(value == null && defaultValue!=null){
ConversionContext ctx = new ConversionContext.Builder("<defaultValue>", targetType).build();
for (PropertyConverter<T> conv : converters) {
try {
value = conv.convert(defaultValue, ctx);
if (value != null) {
break;
}
} catch (Exception e) {
LOG.warning("failed to convert: " + ctx);
}
}
}
return value;
}
@Override
public void setUpdatePolicy(UpdatePolicy updatePolicy) {
this.updatePolicy = Objects.requireNonNull(updatePolicy);
}
@Override
public T getNewValue() {
return newValue;
}
/**
* Performs a commit, if necessary, and returns the current value.
*
* @return the non-null value held by this {@code DynamicValue}
* @throws org.apache.tamaya.ConfigException if there is no value present
* @see DynamicValue#isPresent()
*/
@Override
public T commitAndGet() {
commit();
return get();
}
/**
* Return {@code true} if there is a value present, otherwise {@code false}.
*
* @return {@code true} if there is a value present, otherwise {@code false}
*/
@Override
public boolean isPresent() {
return get() != null;
}
/**
* Return the value if present, otherwise return {@code other}.
*
* @param other the value to be returned if there is no value present, may
* be null
* @return the value, if present, otherwise {@code other}
*/
@Override
public T orElse(T other) {
T value = get();
if (value == null) {
return other;
}
return value;
}
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
*
* @param other a {@code ConfiguredItemSupplier} whose result is returned if no value
* is present
* @return the value if present otherwise the result of {@code other.current()}
* @throws NullPointerException if value is not present and {@code other} is
* null
*/
@Override
public T orElseGet(Supplier<? extends T> other) {
T value = get();
if (value == null) {
return other.get();
}
return value;
}
/**
* Return the contained value, if present, otherwise throw an exception
* to be created by the provided supplier.
* <p>
* NOTE A method reference to the exception constructor with an empty
* argument createList can be used as the supplier. For example,
* {@code IllegalStateException::new}
*
* @param <X> Type of the exception to be thrown
* @param exceptionSupplier The supplier which will return the exception to
* be thrown
* @return the present value
* @throws X if there is no value present
* @throws NullPointerException if no value is present and
* {@code exceptionSupplier} is null
*/
@Override
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
T value = get();
if (value == null) {
throw exceptionSupplier.get();
}
return value;
}
/**
* Simple helper that allows keeping the listeners registered as weak references, hereby avoiding any
* memory leaks.
*
* @param <I> the type
*/
private class WeakList<I> {
final List<WeakReference<I>> refs = new LinkedList<>();
boolean contains(I item){
for(WeakReference<I> t:refs){
if(item.equals(t.get())){
return true;
}
}
return false;
}
void forEach(Consumer<I> consumer){
refs.parallelStream().forEach(ref -> {
I t = ref.get();
if(t!=null){
consumer.accept(t);
}
});
}
/**
* Adds a new instance.
*
* @param t the new instance, not null.
*/
void add(I t) {
refs.add(new WeakReference<>(t));
}
/**
* Removes a instance.
*
* @param t the instance to be removed.
*/
void remove(I t) {
synchronized (refs) {
for (Iterator<WeakReference<I>> iterator = refs.iterator(); iterator.hasNext(); ) {
WeakReference<I> ref = iterator.next();
I instance = ref.get();
if (instance == null || instance == t) {
iterator.remove();
break;
}
}
}
}
/**
* Access a createList (copy) of the current instances that were not discarded by the GC.
*
* @return the createList of accessible items.
*/
public List<I> get() {
synchronized (refs) {
List<I> res = new ArrayList<>();
for (Iterator<WeakReference<I>> iterator = refs.iterator(); iterator.hasNext(); ) {
WeakReference<I> ref = iterator.next();
I instance = ref.get();
if (instance == null) {
iterator.remove();
} else {
res.add(instance);
}
}
return res;
}
}
}
}