blob: b8e0cf5a018b9f3e52af48262eff945eeb526475 [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;
import java.beans.PropertyChangeEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Logger;
/**
* A accessor for a single configured value. This can be used to support values that may change during runtime, reconfigured or
* final. Hereby external code (could be Tamaya configuration listners or client code), can set a new value. Depending on the
* {@link org.apache.tamaya.DynamicValue.UpdatePolicy} the new value is immedeately active or it requires an active commit
* by client code. Similarly an instance also can ignore all later changes to the value.
* <h3>Implementation Details</h3>
* This class is
* <ul>
* <li>Serializable, when also the item stored is serializable</li>
* <li>Thread safe</li>
* </ul>
*/
public final class DynamicValue<T> implements Serializable{
/**
* Policy to control how new values are applied to this instance.
*/
enum UpdatePolicy{
/** New values are applied immedately and registered listeners are informed about the change. */
IMMEDIATE,
/** New values or not applied, but stored in the newValue property. Explcit call to #commit
of #commitAndGet are required to accept the change and inform the listeners about the change.
*/
EXPLCIT,
/**
* New values are always immedately discarded.
*/
NEVER,
/**
* Changes are logged before the are discarded.
*/
LOG_AND_DISCARD
}
/** The property name of the entry. */
private String propertyName;
/**
* 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 current value, never null. */
private transient Optional<T> value;
/** The new value, or null. */
private transient Optional<T> newValue;
/** List of listeners that listen for changes. */
private transient WeakList<Consumer<PropertyChangeEvent>> listeners;
/**
* Returns an empty {@code Optional} instance. No value is present for this
* Optional.
*
* @apiNote Though it may be tempting to do so, avoid testing if an object
* is empty by comparing with {@code ==} against instances returned by
* {@code Option.empty()}. There is no guarantee that it is a singleton.
* Instead, use {@link #isPresent()}.
*
* @param <T> Type of the non-existent value
* @return an empty {@code Optional}
*/
public static <T> DynamicValue<T> empty(String propertyName) {
DynamicValue v = new DynamicValue<T>(propertyName, null);
return v;
}
/**
* Constructor.
* @param propertyName the name of the value in the format {@code <configName>:<propertyName>}.</config>
* @param item the initial value.
*/
private DynamicValue(String propertyName, Optional<T> item){
this.propertyName = Objects.requireNonNull(propertyName);
this.value = item;
}
/**
* Creates a new instance.
* @param propertyName the name of the value in the format {@code <configName>:<propertyName>}.</config>
* @param value the initial value, not null.
* @param <T> the type
* @return a new instance, never null
*/
public static <T> DynamicValue<T> of(String propertyName, T value){
return new DynamicValue(propertyName, Optional.of(value));
}
/**
* Creates a new instance.
* @param propertyName the name of the value in the format {@code <configName>:<propertyName>}.</config>
* @param value the initial value
* @param <T> the target type.
* @return a new instance, never null
*/
public static <T> DynamicValue<T> ofNullable(String propertyName, T value){
return value == null ? empty(propertyName) : of(propertyName, value);
}
/**
* Performs a commit, if necessary and returns the current value.
* otherwise throws {@code ConfigException}.
*
* @return the non-null value held by this {@code Optional}
* @throws org.apache.tamaya.ConfigException if there is no value present
*
* @see DynamicValue#isPresent()
*/
public T commitAndGet(){
commit();
return get();
}
/**
* Commits a new value that has not been committed yet, make it the new value of the instance. On change any registered listeners will be triggered.
*/
public void commit(){
synchronized (value){
if(newValue!=null){
PropertyChangeEvent evt = new PropertyChangeEvent(this, propertyName, value.orElse(null), newValue.orElse(null));
value = newValue;
newValue = null;
for(Consumer<PropertyChangeEvent> consumer: listeners.get()){
consumer.accept(evt);
}
}
}
}
/**
* Discards a new value that was published. No listeners will be informed.
*/
public void discard(){
newValue = null;
}
/**
* Access the {@link UpdatePolicy} used for updating this value.
* @return the update policy, never null.
*/
public UpdatePolicy getUpdatePolicy() {
return updatePolicy;
}
/**
* Add a listener to be called as weak reference, when this value has been changed.
* @param l the listner, not null
*/
public void addListener(Consumer<PropertyChangeEvent> l) {
if(listeners==null){
listeners = new WeakList<>();
}
listeners.add(l);
}
/**
* Removes a listener to be called, when this value has been changed.
* @param l the listner to be removed, not null
*/
public void removeListener(Consumer<PropertyChangeEvent> l) {
if(listeners!=null){
listeners.remove(l);
}
}
/**
* If a value is present in this {@code ConfiguredValue}, returns the value,
* otherwise throws {@code ConfigException}.
*
* @return the non-null value held by this {@code Optional}
* @throws org.apache.tamaya.ConfigException if there is no value present
*
* @see DynamicValue#isPresent()
*/
public T get() {
return value.get();
}
/**
* Method to apply a new value. Depending on the {@link org.apache.tamaya.DynamicValue.UpdatePolicy}
* the value is immediately or deferred visible (or it may even be ignored completely).
* @param newValue the new value, may also be null.
*/
public void setNewValue(T newValue){
switch(this.updatePolicy){
case IMMEDIATE:
this.newValue = Optional.ofNullable(newValue);
commit();
break;
case EXPLCIT:
this.newValue = Optional.ofNullable(newValue);
break;
case LOG_AND_DISCARD:
Logger.getLogger(getClass().getName()).info("Discard change on " + this + ", newValue="+newValue);
this.newValue = null;
break;
case NEVER:
this.newValue = null;
break;
}
}
/**
* Sets a new {@link org.apache.tamaya.DynamicValue.UpdatePolicy}.
* @param updatePolicy the new policy, not null.
*/
public void setUpdatePolicy(UpdatePolicy updatePolicy){
this.updatePolicy = Objects.requireNonNull(updatePolicy);
}
/**
* Access a new value that has not yet been committed.
* @return the uncommitted new value, or null.
*/
public T getNewValue(){
Optional<T> nv = newValue;
if(nv!=null){
return nv.orElse(null);
}
return null;
}
/**
* Return {@code true} if there is a value present, otherwise {@code false}.
*
* @return {@code true} if there is a value present, otherwise {@code false}
*/
public boolean isPresent() {
return value.isPresent();
}
/**
* If a value is present, invoke the specified consumer with the value,
* otherwise do nothing.
*
* @param consumer block to be executed if a value is present
* @throws NullPointerException if value is present and {@code consumer} is
* null
*/
public void ifPresent(Consumer<? super T> consumer) {
value.ifPresent(consumer);
}
/**
* If a value is present, and the value matches the given predicate,
* return an {@code Optional} describing the value, otherwise return an
* empty {@code Optional}.
*
* @param predicate a predicate to apply to the value, if present
* @return an {@code Optional} describing the value of this {@code Optional}
* if a value is present and the value matches the given predicate,
* otherwise an empty {@code Optional}
* @throws NullPointerException if the predicate is null
*/
public DynamicValue<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value.get()) ? this : empty(propertyName);
}
/**
* If a value is present, apply the provided mapping function to it,
* and if the result is non-null, return an {@code Optional} describing the
* result. Otherwise return an empty {@code Optional}.
*
* @apiNote This method supports post-processing on optional values, without
* the need to explicitly check for a return status. For example, the
* following code traverses a stream of file names, selects one that has
* not yet been processed, and then opens that file, returning an
* {@code Optional<FileInputStream>}:
*
* <pre>{@code
* Optional<FileInputStream> fis =
* names.stream().filter(name -> !isProcessedYet(name))
* .findFirst()
* .map(name -> new FileInputStream(name));
* }</pre>
*
* Here, {@code findFirst} returns an {@code Optional<String>}, and then
* {@code map} returns an {@code Optional<FileInputStream>} for the desired
* file if one exists.
*
* @param <U> The type of the result of the mapping function
* @param mapper a mapping function to apply to the value, if present
* @return an {@code Optional} describing the result of applying a mapping
* function to the value of this {@code Optional}, if a value is present,
* otherwise an empty {@code Optional}
* @throws NullPointerException if the mapping function is null
*/
public <U> DynamicValue<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty(propertyName);
else {
return DynamicValue.ofNullable(propertyName, mapper.apply(value.get()));
}
}
/**
* If a value is present, apply the provided {@code Optional}-bearing
* mapping function to it, return that result, otherwise return an empty
* {@code Optional}. This method is similar to {@link #map(Function)},
* but the provided mapper is one whose result is already an {@code Optional},
* and if invoked, {@code flatMap} does not wrap it with an additional
* {@code Optional}.
*
* @param <U> The type parameter to the {@code Optional} returned by
* @param mapper a mapping function to apply to the value, if present
* the mapping function
* @return the result of applying an {@code Optional}-bearing mapping
* function to the value of this {@code Optional}, if a value is present,
* otherwise an empty {@code Optional}
* @throws NullPointerException if the mapping function is null or returns
* a null result
*/
public <U> DynamicValue<U> flatMap(Function<? super T, DynamicValue<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty(propertyName);
else {
return Objects.requireNonNull(mapper.apply(value.get()));
}
}
/**
* 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}
*/
public T orElse(T other) {
return value.orElse(other);
}
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
*
* @param other a {@code Supplier} whose result is returned if no value
* is present
* @return the value if present otherwise the result of {@code other.get()}
* @throws NullPointerException if value is not present and {@code other} is
* null
*/
public T orElseGet(Supplier<? extends T> other) {
return value.orElseGet(other);
}
/**
* Return the contained value, if present, otherwise throw an exception
* to be created by the provided supplier.
*
* @apiNote A method reference to the exception constructor with an empty
* argument list 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
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
return value.orElseThrow(exceptionSupplier);
}
/**
* Converts the instance to an {@link java.util.Optional} instance.
* @return the corresponding Optional value.
*/
public Optional<T> toOptional(){
return value;
}
/**
* Serialization implementation that strips away the non serializable Optional part.
* @param oos the output stream
* @throws IOException if serialization fails.
*/
private void writeObject(ObjectOutputStream oos)throws IOException {
oos.writeObject(updatePolicy);
if(isPresent()) {
oos.writeObject(this.value.get());
}
else{
oos.writeObject(null);
}
}
/**
* Reads an instance from the input stream.
* @param ois the object input stream
* @throws IOException if deserialization fails.
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
this.updatePolicy = (UpdatePolicy)ois.readObject();
if(isPresent()) {
this.value = Optional.of((T) ois.readObject());
}
newValue = null;
}
/**
* Simple helper that allows keeping the listeners registered as weak references, hereby avoiding any
* memory leaks.
* @param <T> the type
*/
private class WeakList<T>{
List<WeakReference<T>> refs = new LinkedList<>();
/**
* Adds a new instance.
* @param t the new instance, not null.
*/
void add(T t){
refs.add(new WeakReference(t));
}
/**
* Removes a instance.
* @param t the instance to be removed.
*/
void remove(T t){
synchronized (refs){
for(Iterator<WeakReference<T>> iterator = refs.iterator();iterator.hasNext();){
WeakReference<T> ref = iterator.next();
T instance = ref.get();
if(instance==null || instance == t){
iterator.remove();
break;
}
}
}
}
/**
* Access a list (copy) of the current instances that were not discarded by the GC.
* @return the list of accessible items.
*/
public List<T> get() {
synchronized (refs) {
List<T> res = new ArrayList<>();
for (Iterator<WeakReference<T>> iterator = refs.iterator(); iterator.hasNext(); ) {
WeakReference<T> ref = iterator.next();
T instance = ref.get();
if(instance==null){
iterator.remove();
}
else{
res.add(instance);
}
}
return res;
}
}
}
}