| /* |
| * 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.geronimo.config; |
| |
| import java.lang.reflect.ParameterizedType; |
| import java.lang.reflect.Type; |
| import java.net.URL; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.stream.Collectors; |
| |
| import org.apache.geronimo.config.converters.BooleanConverter; |
| import org.apache.geronimo.config.converters.DoubleConverter; |
| import org.apache.geronimo.config.converters.DurationConverter; |
| import org.apache.geronimo.config.converters.FloatConverter; |
| import org.apache.geronimo.config.converters.ImplicitConverter; |
| import org.apache.geronimo.config.converters.IntegerConverter; |
| import org.apache.geronimo.config.converters.LongConverter; |
| import org.apache.geronimo.config.converters.StringConverter; |
| import org.apache.geronimo.config.converters.URLConverter; |
| import javax.config.Config; |
| import javax.config.ConfigSnapshot; |
| import javax.config.ConfigAccessor; |
| import javax.config.spi.ConfigSource; |
| import javax.config.spi.Converter; |
| |
| import javax.annotation.Priority; |
| import javax.enterprise.inject.Typed; |
| import javax.enterprise.inject.Vetoed; |
| |
| /** |
| * @author <a href="mailto:struberg@apache.org">Mark Struberg</a> |
| * @author <a href="mailto:johndament@apache.org">John D. Ament</a> |
| */ |
| @Typed |
| @Vetoed |
| public class ConfigImpl implements Config, AutoCloseable { |
| protected Logger logger = Logger.getLogger(ConfigImpl.class.getName()); |
| |
| protected List<ConfigSource> configSources = new ArrayList<>(); |
| protected Map<Type, Converter> converters = new HashMap<>(); |
| protected Map<Type, Converter> implicitConverters = new ConcurrentHashMap<>(); |
| |
| // volatile to a.) make the read/write behave atomic and b.) guarantee multi-thread safety |
| private volatile long lastChanged = 0; |
| |
| |
| public ConfigImpl() { |
| registerDefaultConverter(); |
| } |
| |
| private void registerDefaultConverter() { |
| converters.put(String.class, StringConverter.INSTANCE); |
| converters.put(Boolean.class, BooleanConverter.INSTANCE); |
| converters.put(boolean.class, BooleanConverter.INSTANCE); |
| converters.put(Double.class, DoubleConverter.INSTANCE); |
| converters.put(double.class, DoubleConverter.INSTANCE); |
| converters.put(Float.class, FloatConverter.INSTANCE); |
| converters.put(float.class, FloatConverter.INSTANCE); |
| converters.put(Integer.class, IntegerConverter.INSTANCE); |
| converters.put(int.class, IntegerConverter.INSTANCE); |
| converters.put(Long.class, LongConverter.INSTANCE); |
| converters.put(long.class, LongConverter.INSTANCE); |
| |
| converters.put(Duration.class, DurationConverter.INSTANCE); |
| |
| converters.put(URL.class, URLConverter.INSTANCE); |
| } |
| |
| |
| @Override |
| public <T> Optional<T> getOptionalValue(String propertyName, Class<T> asType) { |
| String value = getValue(propertyName); |
| if (value != null && value.length() == 0) { |
| // treat an empty string as not existing |
| value = null; |
| } |
| return Optional.ofNullable(convert(value, asType)); |
| } |
| |
| @Override |
| public ConfigSnapshot snapshotFor(ConfigAccessor<?>... configValues) { |
| // we implement kind of optimistic Locking |
| // Means we try multiple time to resolve all the given values |
| // until the config didn't change inbetween. |
| for (int tries = 1; tries < 5; tries++) |
| { |
| Map<ConfigAccessor<?>, Object> resolved = new HashMap<>(); |
| long startReadLastChanged = lastChanged; |
| for (ConfigAccessor configValue : configValues) |
| { |
| resolved.put(configValue, configValue.getValue()); |
| } |
| |
| if (startReadLastChanged == lastChanged) |
| { |
| return new ConfigSnapshotImpl(resolved); |
| } |
| } |
| |
| throw new IllegalStateException( |
| "Could not resolve ConfigTransaction as underlying values are permanently changing!"); |
| } |
| |
| @Override |
| public <T> T getValue(String propertyName, Class<T> propertyType) { |
| String value = getValue(propertyName); |
| if (value == null) { |
| throw new NoSuchElementException("No configured value found for config key " + propertyName); |
| } |
| |
| return convert(value, propertyType); |
| } |
| |
| public String getValue(String key) { |
| for (ConfigSource configSource : configSources) { |
| String value = configSource.getValue(key); |
| |
| if (value != null) { |
| if (logger.isLoggable(Level.FINE)) { |
| logger.log(Level.FINE, "found value {0} for key {1} in ConfigSource {2}.", |
| new Object[]{value, key, configSource.getName()}); |
| } |
| |
| return value; |
| } |
| } |
| |
| return null; |
| } |
| |
| public <T> T convert(String value, Class<T> asType) { |
| if (value != null) { |
| Converter<T> converter = getConverter(asType); |
| return converter.convert(value); |
| } |
| |
| return null; |
| } |
| |
| private <T> Converter getConverter(Class<T> asType) { |
| Converter converter = converters.get(asType); |
| if (converter == null) { |
| converter = getImplicitConverter(asType); |
| } |
| if (converter == null) { |
| throw new IllegalArgumentException("No Converter registered for class " + asType); |
| } |
| return converter; |
| } |
| |
| private <T> Converter getImplicitConverter(Class<T> asType) { |
| Converter converter = implicitConverters.get(asType); |
| if (converter == null) { |
| synchronized (implicitConverters) { |
| converter = implicitConverters.get(asType); |
| if (converter == null) { |
| if (asType.isArray()) { |
| Converter singleItemConverter = getConverter(asType.getComponentType()); |
| if (singleItemConverter == null) { |
| return null; |
| } |
| else { |
| converter = new ImplicitConverter.ImplicitArrayConverter(singleItemConverter, asType.getComponentType()); |
| implicitConverters.putIfAbsent(asType, converter); |
| } |
| } |
| else { |
| // try to check whether the class is an 'implicit converter' |
| converter = ImplicitConverter.getImplicitConverter(asType); |
| if (converter != null) { |
| implicitConverters.putIfAbsent(asType, converter); |
| } |
| } |
| } |
| } |
| } |
| return converter; |
| } |
| |
| public <T> ConfigValueImpl<T> access(String key, Class<T> type) { |
| return new ConfigValueImpl<T>(this, key, type); |
| } |
| |
| @Override |
| public Iterable<String> getPropertyNames() { |
| return configSources.stream().flatMap(c -> c.getPropertyNames().stream()).collect(Collectors.toSet()); |
| } |
| |
| @Override |
| public Iterable<ConfigSource> getConfigSources() { |
| return Collections.unmodifiableList(configSources); |
| } |
| |
| public synchronized void addConfigSources(List<ConfigSource> configSourcesToAdd) { |
| List<ConfigSource> allConfigSources = new ArrayList<>(configSources); |
| for (ConfigSource configSource : configSourcesToAdd) { |
| configSource.setAttributeChangeCallback(this::onAttributeChange); |
| allConfigSources.add(configSource); |
| } |
| |
| // finally put all the configSources back into the map |
| configSources = sortDescending(allConfigSources); |
| } |
| |
| |
| public synchronized void addConverter(Converter<?> converter) { |
| if (converter == null) { |
| return; |
| } |
| |
| Type targetType = getTypeOfConverter(converter.getClass()); |
| if (targetType == null ) { |
| throw new IllegalStateException("Converter " + converter.getClass() + " must be a ParameterisedType"); |
| } |
| |
| Converter oldConverter = converters.get(targetType); |
| if (oldConverter == null || getPriority(converter) > getPriority(oldConverter)) { |
| converters.put(targetType, converter); |
| } |
| } |
| |
| public void addPrioritisedConverter(DefaultConfigBuilder.PrioritisedConverter prioritisedConverter) { |
| Converter oldConverter = converters.get(prioritisedConverter.getType()); |
| if (oldConverter == null || prioritisedConverter.getPriority() >= getPriority(oldConverter)) { |
| converters.put(prioritisedConverter.getType(), prioritisedConverter.getConverter()); |
| } |
| } |
| |
| |
| private int getPriority(Converter<?> converter) { |
| int priority = 100; |
| Priority priorityAnnotation = converter.getClass().getAnnotation(Priority.class); |
| if (priorityAnnotation != null) { |
| priority = priorityAnnotation.value(); |
| } |
| return priority; |
| } |
| |
| |
| public Map<Type, Converter> getConverters() { |
| return converters; |
| } |
| |
| |
| @Override |
| public void close() throws Exception { |
| List<Exception> exceptions = new ArrayList<>(); |
| |
| converters.values().stream() |
| .filter(c -> c instanceof AutoCloseable) |
| .map(AutoCloseable.class::cast) |
| .forEach(c -> { |
| try { |
| c.close(); |
| } |
| catch (Exception e) { |
| exceptions.add(e); |
| } |
| }); |
| |
| configSources.stream() |
| .filter(c -> c instanceof AutoCloseable) |
| .map(AutoCloseable.class::cast) |
| .forEach(c -> { |
| try { |
| c.close(); |
| } |
| catch (Exception e) { |
| exceptions.add(e); |
| } |
| }); |
| |
| if (!exceptions.isEmpty()) { |
| StringBuilder sb = new StringBuilder(1024); |
| sb.append("The following Exceptions got detected while shutting down the Config:\n"); |
| for (Exception exception : exceptions) { |
| sb.append(exception.getClass().getName()) |
| .append(" ") |
| .append(exception.getMessage()) |
| .append('\n'); |
| } |
| |
| throw new RuntimeException(sb.toString(), exceptions.get(0)); |
| } |
| } |
| |
| /** |
| * ConfigSources are sorted with descending ordinal. |
| * If 2 ConfigSources have the same ordinal, then they get sorted according to their name, alphabetically. |
| */ |
| private List<ConfigSource> sortDescending(List<ConfigSource> configSources) { |
| configSources.sort( |
| (configSource1, configSource2) -> { |
| int compare = Integer.compare(configSource2.getOrdinal(), configSource1.getOrdinal()); |
| if (compare == 0) { |
| return configSource1.getName().compareTo(configSource2.getName()); |
| } |
| return compare; |
| }); |
| return configSources; |
| } |
| |
| private Type getTypeOfConverter(Class clazz) { |
| if (clazz.equals(Object.class)) { |
| return null; |
| } |
| |
| Type[] genericInterfaces = clazz.getGenericInterfaces(); |
| for (Type genericInterface : genericInterfaces) { |
| if (genericInterface instanceof ParameterizedType) { |
| ParameterizedType pt = (ParameterizedType) genericInterface; |
| if (pt.getRawType().equals(Converter.class)) { |
| Type[] typeArguments = pt.getActualTypeArguments(); |
| if (typeArguments.length != 1) { |
| throw new IllegalStateException("Converter " + clazz + " must be a ParameterisedType"); |
| } |
| return typeArguments[0]; |
| } |
| } |
| } |
| |
| return getTypeOfConverter(clazz.getSuperclass()); |
| } |
| |
| public void onAttributeChange(Set<String> attributesChanged) |
| { |
| // this is to force an incremented lastChanged even on time glitches and fast updates |
| long newLastChanged = System.nanoTime(); |
| lastChanged = lastChanged >= newLastChanged ? lastChanged++ : newLastChanged; |
| } |
| |
| /** |
| * @return the nanoTime when the last change got reported by a ConfigSource |
| */ |
| public long getLastChanged() |
| { |
| return lastChanged; |
| } |
| |
| |
| } |