AbstractFileLocationStrategy.java
/*
* 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
*
* https://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.commons.configuration2.io;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.configuration2.ex.ConfigurationDeniedException;
import org.apache.commons.io.build.AbstractSupplier;
import org.apache.commons.lang3.StringUtils;
/**
* Abstracts services for FileLocationStrategy implementations.
* <p>
* Note that some FileLocationStrategy implementation use URLs internally to encode file locations.
* </p>
* <p>
* As of version 2.15.0, by default, the only URL schemes allowed are {@code file} and {@code jar}. To override this default, you can either use the system
* property {@code org.apache.commons.configuration2.io.FileLocationStrategy.schemes} or build a subclass of {@link AbstractFileLocationStrategy}.
* </p>
* <strong>Using System Properties</strong>
* <p>
* The system property {@code org.apache.commons.configuration2.io.FileLocationStrategy.schemes} String value must be a comma-separated list of schemes,
* where the default is {@code "file,jar"}, and the complete list is {@code "file,http,https,jar"}.
* </p>
* <strong>Using a Builder</strong>
* <p>
* The root builder for {@link AbstractFileLocationStrategy} is {@link AbstractBuilder} where you define allowed schemes and hosts through its setter
* methods.
* </p>
* <p>
* For example, to programatically enable the shemes "file", "http", "https", and "jar" for all strategies, you write:
* </p>
* <pre>{@code
* final PropertiesConfiguration pc = new PropertiesConfiguration();
* pc.setIncludeListener(PropertiesConfiguration.NOOP_INCLUDE_LISTENER);
* final FileHandler handler = new FileHandler(pc);
* final CombinedLocationStrategy.Builder builder = new CombinedLocationStrategy.Builder()
* .setSchemes(new TreeSet<>(Arrays.asList("file", "http", "https", "jar")));
* // @formatter:off
* handler.setLocationStrategy(builder.setSubStrategies(Arrays.asList(
* new ProvidedURLLocationStrategy(builder),
* new FileSystemLocationStrategy(builder),
* new AbsoluteNameLocationStrategy(builder),
* new BasePathLocationStrategy(builder),
* new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(true).getUnchecked(),
* new HomeDirectoryLocationStrategy.Builder().setEvaluateBasePath(false).getUnchecked(),
* new ClasspathLocationStrategy(builder)))
* .get());
* // @formatter:on
* handler.setBasePath(TEST_BASE_PATH);
* handler.setFileName("include-load-url-host-unknown-exception.properties");
* handler.load();
* }</pre>
*
*
* @since 2.15.0
* @see FileLocationStrategy
*/
public abstract class AbstractFileLocationStrategy implements FileLocationStrategy {
/**
* Builds new instances for subclasses.
* <p>
* As of version 2.15.0, by default, the only URL schemes allowed are {@code file} and {@code jar}. To override this default, you can either use the system
* property {@code org.apache.commons.configuration2.io.FileLocationStrategy.schemes} or build a subclass of {@link AbstractFileLocationStrategy}.
* </p>
* <strong>Using System Properties</strong>
* <p>
* The system property {@code org.apache.commons.configuration2.io.FileLocationStrategy.schemes} String value must be a comma-separated list of schemes,
* where the default is {@code "file,jar"}, and the complete list is {@code "file,http,https,jar"}.
* </p>
* <strong>Using a Builder</strong>
* <p>
* The root builder for {@link AbstractFileLocationStrategy} is {@link AbstractBuilder} where you define allowed schemes and hosts through its setter
* methods.
* </p>
* <p>
* See {@link AbstractFileLocationStrategy} learn how to grant an deny URL schemes and hosts.
* </p>
*
* @param <T> The type of {@link FileLocationStrategy} to build.
* @param <B> The builder type.
*/
public abstract static class AbstractBuilder<T extends FileLocationStrategy, B extends AbstractBuilder<T, B>> extends AbstractSupplier<T, B> {
/**
* Enabled URL-based hosts, empty means all are enabled. Host are case-insensitive.
*/
private Set<Pattern> hosts = Collections.emptySet();
/**
* Enabled URL-based schemes, empty means all are enabled. Schemes are case-insensitive.
*/
private Set<String> schemes = Collections.emptySet();
/**
* Constructs a new instance for subclasses.
*/
public AbstractBuilder() {
// empty
}
Set<Pattern> getHosts() {
return hosts;
}
Set<String> getSchemes() {
return schemes;
}
/**
* Sets enabled URL-based hosts, empty means all are enabled. URL hosts are case-insensitive.
*
* @param hosts enabled URL-based hosts.
* @return {@code this} instance.
*/
public B setHosts(final Set<Pattern> hosts) {
this.hosts = hosts != null ? hosts : Collections.emptySet();
return asThis();
}
/**
* Sets enabled URL-based hosts, empty means all are enabled. URL hosts are case-insensitive.
*
* @param hosts Regular expressions enabled URL-based hosts.
* @return {@code this} instance.
*/
public B setHostsRegEx(final Set<String> hosts) {
return setHosts(hosts.stream().map(e -> Pattern.compile(e, Pattern.CASE_INSENSITIVE)).collect(Collectors.toSet()));
}
/**
* Sets enabled URL-based schemes, empty means all are enabled. URL schemes are case-insensitive.
*
* @param schemes enabled URL-based schemes, the default null means all schemes are allowed.
* @return {@code this} instance.
*/
public B setSchemes(final Set<String> schemes) {
this.schemes = schemes != null ? schemes : Collections.emptySet();
return asThis();
}
}
/**
* Builds new instances of T.
*
* @param <T> The type of {@link FileLocationStrategy} to build.
*/
public static class StrategyBuilder<T extends FileLocationStrategy> extends AbstractBuilder<T, StrategyBuilder<T>> {
/**
* Either set this or implement get().
*/
private final Function<StrategyBuilder<T>, T> function;
/**
* Constructs a new instance for subclasses.
*
* @param function Builds an instance of T.
*/
public StrategyBuilder(final Function<StrategyBuilder<T>, T> function) {
this.function = Objects.requireNonNull(function, "function");
}
@Override
public T get() {
return function.apply(asThis());
}
}
/**
* Default schemes.
*/
private static final String DEFAULT_SCHEMES = "file,jar";
/**
* The system property key {@code org.apache.commons.configuration2.io.FileLocationStrategy.schemes}.
* <p>
* If absent, defaults to {@code "file,jar"}.
* </p>
* <p>
* For complete functionality, use {@code "file,http,https,jar"}.
* </p>
*/
private static final String KEY_SCHEMES = "org.apache.commons.configuration2.io.FileLocationStrategy.schemes";
private static void checkHost(final String value, final Set<Pattern> validSet) {
final String lowerCase = StringUtils.toRootLowerCase(value);
if (!validSet.isEmpty() && StringUtils.isNotEmpty(lowerCase) && validSet.stream().noneMatch(p -> p.matcher(lowerCase).matches())) {
throw new ConfigurationDeniedException("URL host is not enabled: %s; must be one of %s", value, validSet);
}
}
/**
* Checks if the scheme is allowed.
*
* @param value A URL scheme, never empty or {@code null}.
* @param validSet The scheme valid-set.
*/
private static void checkScheme(final String value, final Set<String> validSet) {
if (!validSet.isEmpty() && !validSet.contains(StringUtils.toRootLowerCase(value))) {
throw new ConfigurationDeniedException("URL scheme \"%s\" is not enabled, must be one of %s, override defaults with the system property \"%s\", "
+ "complete set: \"file,http,https,jar\"", value, validSet, KEY_SCHEMES);
}
}
/**
* Validates {@code url} against the scheme and host allow-lists.
*
* @param url The URL to check.
* @param validSchemes The scheme valid-set.
* @param validHosts The host valid-set.
* @throws ConfigurationDeniedException if the URL or any embedded URL fails the check, or a {@code jar:} URL is malformed.
*/
static void checkUrl(final URL url, final Set<String> validSchemes, final Set<Pattern> validHosts) {
final String scheme = url.getProtocol();
checkScheme(scheme, validSchemes);
if ("jar".equalsIgnoreCase(scheme)) {
try {
// Follows the logic of JarURLConnection#parseSpecs without the cost of opening a connection.
final String spec = url.getFile();
final int sep = spec.lastIndexOf("!/");
if (sep < 0) {
throw new MalformedURLException("no !/ found in url spec:" + spec);
}
final URL inner = new URL(spec.substring(0, sep));
checkUrl(inner, validSchemes, validHosts);
} catch (final MalformedURLException e) {
throw new ConfigurationDeniedException(e, "Malformed 'jar:' URL: %s", url);
}
} else {
checkHost(url.getHost(), validHosts);
}
}
private static Set<String> getSchemesProperty() {
final Set<String> set = new LinkedHashSet<>();
final String[] split = System.getProperty(KEY_SCHEMES, DEFAULT_SCHEMES).split(",");
Collections.addAll(set, split);
return set;
}
/**
* Enabled URL-based hosts, empty means all are enabled. Host are case-insensitive.
*/
private final Set<Pattern> hosts;
/**
* Enabled URL-based schemes, empty means all are enabled. Schemes are case-insensitive.
*/
private final Set<String> schemes;
/**
* Constructs a new instance where the enabled URL schemes are read the system property
* {@code "org.apache.commons.configuration2.io.FileLocationStrategy.schemes"}.
* <p>
* If absent, defaults to {@code "file,jar"}.
* </p>
* <p>
* For complete functionality, use {@code "file,http,https,jar"}.
* </p>
*/
AbstractFileLocationStrategy() {
this(getSchemesProperty());
}
AbstractFileLocationStrategy(final AbstractBuilder<?, ?> builder) {
Objects.requireNonNull(builder, "builder");
this.schemes = builder.schemes;
this.hosts = builder.hosts != null ? builder.hosts : Collections.emptySet();
}
AbstractFileLocationStrategy(final Set<String> schemes) {
this.schemes = schemes;
this.hosts = Collections.emptySet();
}
URL check(final URL url) {
if (url != null) {
checkUrl(url, schemes, hosts);
}
return url;
}
/**
* Gets the enabled hosts.
*
* @return The enabled hosts.
*/
Set<Pattern> getHosts() {
return hosts;
}
/**
* Gets the enabled schemes.
*
* @return The enabled schemes.
*/
Set<String> getSchemes() {
return schemes;
}
@Override
public String toString() {
return getClass().getSimpleName() + " [schemes=" + schemes + ", hosts=" + hosts + "]";
}
}