io.helidon.config.ConfigUtils Maven / Gradle / Ivy
Show all versions of helidon-config Show documentation
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed 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 io.helidon.config;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.time.Duration;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Priority;
import io.helidon.config.spi.ConfigNode;
/**
* Internal config utilities.
*/
final class ConfigUtils {
private static final Logger LOGGER = Logger.getLogger(ConfigUtils.class.getName());
private ConfigUtils() {
throw new AssertionError("Instantiation not allowed.");
}
/**
* Convert iterable items to an ordered serial stream.
*
* @param items items to be streamed.
* @param expected streamed item type.
* @return stream of items.
*/
static Stream asStream(Iterable items) {
return asStream(items.iterator());
}
/**
* Converts an iterator to a stream.
*
* @param type of the base items
* @param iterator iterator over the items
* @return stream of the items
*/
static Stream asStream(Iterator iterator) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false);
}
/**
* Sorts items represented by an {@link Iterable} instance based on a {@code Priority} annotation attached to each
* item's Java type and return the sorted items as an ordered serial {@link Stream}.
*
* Instances are sorted by {@code Priority.value()} attached directly to the instance of each item's class.
* If there is no {@code Priority} annotation attached to an item's class the {@code defaultPriority} value is used
* instead. Items with higher priority values have higher priority and take precedence (are returned sooner from
* the stream).
*
* @param items items to be ordered by priority and streamed.
* @param defaultPriority default priority to be used in case an item does not have a priority defined.
* @param item type.
* @return prioritized stream of items.
*/
static Stream asPrioritizedStream(Iterable items, int defaultPriority) {
return asStream(items).sorted(priorityComparator(defaultPriority));
}
/**
* Returns a comparator for two objects, the classes for which are
* optionally annotated with {@link Priority} and which applies a specified
* default priority if either or both classes lack the annotation.
*
* @param type of object being compared
* @param defaultPriority used if the classes for either or both objects
* lack the {@code Priority} annotation
* @return comparator
*/
static Comparator priorityComparator(int defaultPriority) {
return (service1, service2) -> {
int service1Priority = Optional.ofNullable(service1.getClass().getAnnotation(Priority.class))
.map(Priority::value)
.orElse(defaultPriority);
int service2Priority = Optional.ofNullable(service2.getClass().getAnnotation(Priority.class))
.map(Priority::value)
.orElse(defaultPriority);
return service2Priority - service1Priority;
};
}
/**
* Builds map into object node.
*
* Dots in keys are interpreted as tree-structure separators.
*
* @param map source map
* @param strict In strict mode, properties overlapping causes failure during loading into internal structure.
* @return built object node from map source.
*/
static ConfigNode.ObjectNode mapToObjectNode(Map map, boolean strict) {
ConfigNode.ObjectNode.Builder builder = ConfigNode.ObjectNode.builder();
for (Map.Entry entry : map.entrySet()) {
try {
builder.addValue(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
} catch (ConfigException ex) {
if (strict) {
throw ex;
} else {
LOGGER.log(Level.CONFIG, "Tree-structure failure on key '" + entry.getKey() + "', reason: "
+ ex.getLocalizedMessage());
LOGGER.log(Level.FINEST, "Detailed reason of failure of adding key '" + entry.getKey()
+ "' = '" + entry.getValue() + "'.", ex);
}
}
}
return builder.build();
}
/**
* Transforms {@link java.util.Properties} to {@code Map}.
*
* It iterates just {@link Properties#stringPropertyNames() string property names} and uses it's
* {@link Properties#getProperty(String) string value}.
*
* @param properties properties to be transformed to map
* @return transformed map
*/
static Map propertiesToMap(Properties properties) {
return properties.stringPropertyNames().stream()
.collect(Collectors.toMap(k -> k, properties::getProperty));
}
/**
* Shutdowns {@code executor} and waits for it.
*
* @param executor executor to be shutdown.
*/
static void shutdownExecutor(ScheduledExecutorService executor) {
executor.shutdown();
try {
executor.awaitTermination(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
/**
* Returns a {@link Charset} instance parsed from specified {@code content-encoding} HTTP response header
* or {@code UTF-8} if the header is missing.
*
* @param contentEncoding {@code content-type} HTTP response header
* @return {@link Charset} parsed from {@code contentEncoding}
* or {@code UTF-8} in case a {@code contentEncoding} is {@code null}
* @throws ConfigException in case of unsupported charset name
*/
static Charset getContentCharset(String contentEncoding) throws ConfigException {
try {
return Optional.ofNullable(contentEncoding)
.map(Charset::forName)
.orElse(StandardCharsets.UTF_8);
} catch (UnsupportedCharsetException ex) {
throw new ConfigException("Unsupported response content-encoding '" + contentEncoding + "'.", ex);
}
}
/**
* Allows to {@link #schedule()} execution of specified {@code command} using specified {@link ScheduledExecutorService}.
* Task is not executed immediately but scheduled with specified {@code delay}.
* It is possible to postpone an execution of the command by calling {@link #schedule()} again before the command is finished.
*
* It can be used to implement Rx Debounce operator (http://reactivex.io/documentation/operators/debounce.html).
*/
static class ScheduledTask {
private final ScheduledExecutorService executorService;
private final Runnable command;
private final Duration delay;
private volatile ScheduledFuture scheduled;
private final Object lock = new Object();
/**
* Initialize task.
*
* @param executorService service to be used to schedule {@code command} execution on
* @param command the command to be executed on {@code executorService}
* @param delay the {@code command} is scheduled with specified delay
*/
ScheduledTask(ScheduledExecutorService executorService, Runnable command, Duration delay) {
this.executorService = executorService;
this.command = command;
this.delay = delay;
}
/**
* Schedule execution of {@code command} on specified {@code executorService} with initial {@code delay}.
*
* Scheduling can be repeated. Not finished task is canceled.
*
* @return whether a previously-scheduled action was canceled in scheduling this new new action
*/
public boolean schedule() {
boolean result = false;
synchronized (lock) {
if (scheduled != null) {
if (!scheduled.isCancelled() && !scheduled.isDone()) {
scheduled.cancel(false);
LOGGER.log(Level.FINER, String.format("Cancelling and rescheduling %s task.", command));
result = true;
}
}
scheduled = executorService.schedule(command, delay.toMillis(), TimeUnit.MILLISECONDS);
}
return result;
}
}
}