| /* |
| * 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; |
| } |
| } |
| } |
| |
| } |