io.helidon.config.UrlConfigSource Maven / Gradle / Ivy
Show all versions of helidon-config Show documentation
/*
* Copyright (c) 2019, 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.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.spi.ChangeWatcher;
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigParser.Content;
import io.helidon.config.spi.ConfigSource;
import io.helidon.config.spi.ParsableSource;
import io.helidon.config.spi.PollableSource;
import io.helidon.config.spi.PollingStrategy;
import io.helidon.config.spi.WatchableSource;
/**
* {@link ConfigSource} implementation that loads configuration content from specified endpoint URL.
*
* @see AbstractConfigSourceBuilder
*/
public final class UrlConfigSource extends AbstractConfigSource
implements WatchableSource, ParsableSource, PollableSource {
private static final Logger LOGGER = Logger.getLogger(UrlConfigSource.class.getName());
private static final String GET_METHOD = "GET";
private static final String URL_KEY = "url";
private static final int STATUS_NOT_FOUND = 404;
private final URL url;
private UrlConfigSource(Builder builder) {
super(builder);
this.url = builder.url;
}
/**
* Initializes config source instance from configuration properties.
*
* Mandatory {@code properties}, see {@link io.helidon.config.ConfigSources#url(URL)}:
*
* - {@code url} - type {@link URL}
*
* Optional {@code properties}: see {@link AbstractConfigSourceBuilder#config(Config)}.
*
* @param metaConfig meta-configuration used to initialize returned config source instance from.
* @return new instance of config source described by {@code metaConfig}
* @throws MissingValueException in case the configuration tree does not contain all expected sub-nodes
* required by the mapper implementation to provide instance of Java type.
* @throws ConfigMappingException in case the mapper fails to map the (existing) configuration tree represented by the
* supplied configuration node to an instance of a given Java type.
* @see io.helidon.config.ConfigSources#url(URL)
* @see AbstractConfigSourceBuilder#config(Config)
*/
public static UrlConfigSource create(Config metaConfig) throws ConfigMappingException, MissingValueException {
return builder()
.config(metaConfig)
.build();
}
/**
* A new fluent API builder.
*
* @return a new builder instance
*/
public static Builder builder() {
return new Builder();
}
@Override
protected String uid() {
return url.toString();
}
@Override
public URL target() {
return url;
}
@Override
public Class targetType() {
return URL.class;
}
@Override
public Optional parser() {
return super.parser();
}
@Override
public Optional mediaType() {
return super.mediaType();
}
@Override
public Optional pollingStrategy() {
return super.pollingStrategy();
}
@Override
public Optional> changeWatcher() {
return super.changeWatcher();
}
@Override
public boolean isModified(Instant stamp) {
return UrlHelper.isModified(url, stamp);
}
@Override
public Optional load() throws ConfigException {
try {
URLConnection urlConnection = url.openConnection();
if (urlConnection instanceof HttpURLConnection) {
return httpContent((HttpURLConnection) urlConnection);
} else {
return genericContent(urlConnection);
}
} catch (ConfigException ex) {
throw ex;
} catch (Exception ex) {
throw new ConfigException("Configuration at url '" + url + "' is not accessible.", ex);
}
}
private Optional genericContent(URLConnection urlConnection) throws IOException {
InputStream is = urlConnection.getInputStream();
Content.Builder builder = Content.builder()
.data(is)
.stamp(Instant.now());
this.probeContentType().ifPresent(builder::mediaType);
return Optional.ofNullable(builder.build());
}
private Optional httpContent(HttpURLConnection connection) throws IOException {
connection.setRequestMethod(GET_METHOD);
try {
connection.connect();
} catch (IOException e) {
// considering this to be unavailable
LOGGER.log(Level.FINEST, "Failed to connect to " + url + ", considering this source to be missing", e);
return Optional.empty();
}
if (STATUS_NOT_FOUND == connection.getResponseCode()) {
return Optional.empty();
}
Optional mediaType = mediaType(connection.getContentType());
final Instant timestamp;
if (connection.getLastModified() == 0) {
timestamp = Instant.now();
LOGGER.fine("Missing GET '" + url + "' response header 'Last-Modified'. Used current time '"
+ timestamp + "' as a content timestamp.");
} else {
timestamp = Instant.ofEpochMilli(connection.getLastModified());
}
InputStream inputStream = connection.getInputStream();
Charset charset = ConfigUtils.getContentCharset(connection.getContentEncoding());
Content.Builder builder = Content.builder();
builder.data(inputStream);
builder.charset(charset);
builder.stamp(timestamp);
mediaType.ifPresent(builder::mediaType);
return Optional.of(builder.build());
}
private Optional mediaType(String responseMediaType) {
return mediaType()
.or(() -> Optional.ofNullable(responseMediaType))
.or(() -> {
Optional mediaType = probeContentType();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("HTTP response does not contain content-type, used guessed one: " + mediaType + ".");
}
return mediaType;
});
}
private Optional probeContentType() {
return MediaTypes.detectType(url);
}
/**
* Url ConfigSource Builder.
*
* It allows to configure following properties:
*
* - {@code url} - configuration endpoint URL;
* - {@code mandatory} - is existence of configuration resource mandatory (by default) or is {@code optional}?
* - {@code media-type} - configuration content media type to be used to look for appropriate {@link ConfigParser};
* - {@code parser} - or directly set {@link ConfigParser} instance to be used to parse the source;
*
*
* If {@code media-type} not set it uses HTTP response header {@code content-type}.
* If {@code media-type} not returned it tries to guess it from url suffix.
*/
public static final class Builder extends AbstractConfigSourceBuilder
implements PollableSource.Builder,
WatchableSource.Builder,
ParsableSource.Builder,
io.helidon.common.Builder {
private URL url;
/**
* Initialize builder.
*/
private Builder() {
}
/**
* URL of the configuration.
*
* @param url of configuration source
* @return updated builder instance
*/
public Builder url(URL url) {
this.url = url;
return this;
}
/**
* {@inheritDoc}
*
* - {@code url} - URL of the configuration source
*
* @param metaConfig configuration properties used to configure a builder instance.
* @return updated builder instance
*/
@Override
public Builder config(Config metaConfig) {
metaConfig.get(URL_KEY).as(URL.class).ifPresent(this::url);
return super.config(metaConfig);
}
/**
* Builds new instance of Url ConfigSource.
*
* If {@code media-type} not set it tries to use {@code content-type} response header or guesses it from file extension.
*
* @return new instance of Url ConfigSource.
*/
@Override
public UrlConfigSource build() {
if (null == url) {
throw new IllegalArgumentException("url must be provided");
}
return new UrlConfigSource(this);
}
@Override
public Builder parser(ConfigParser parser) {
return super.parser(parser);
}
@Override
public Builder mediaType(String mediaType) {
return super.mediaType(mediaType);
}
@Override
public Builder changeWatcher(ChangeWatcher changeWatcher) {
return super.changeWatcher(changeWatcher);
}
@Override
public Builder pollingStrategy(PollingStrategy pollingStrategy) {
return super.pollingStrategy(pollingStrategy);
}
}
}