/*
* #%L
* Gravia :: Runtime :: API
* %%
* Copyright (C) 2013 - 2014 JBoss by Red Hat
* %%
* 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.
* #L%
*/
package org.jboss.gravia.runtime.spi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A {@link PropertiesProvider} that is applying placeholder substitution based on the property values of an external {@link PropertiesProvider}.
*/
public class SubstitutionPropertiesProvider extends CompositePropertiesProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(SubstitutionPropertiesProvider.class);
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([a-zA-Z0-9\\.\\-]+)}");
private static final String BOX_FORMAT = "\\$\\{%s\\}";
private static final String UNESCAPED_BOX_FORMAT = "${%s}";
public SubstitutionPropertiesProvider(PropertiesProvider... delegates) {
super(delegates);
}
@Override
public Object getProperty(String key) {
return getProperty(key, null);
}
@Override
public Object getProperty(String key, Object defaultValue) {
for (PropertiesProvider provider : getDelegates()) {
try {
Object rawValue = provider.getProperty(key);
if (rawValue != null && !isCyclicReference(key, rawValue)) {
return substitute(String.valueOf(rawValue), new HashSet<String>());
}
} catch (Exception e) {
LOGGER.debug("Skipping properties provider:{}, due to:{}", provider, e.getMessage());
}
}
return defaultValue;
}
/**
* Substitutes placeholders in the specified string, taking loops into consideration.
* It uses the delegating property providers to resolve the placeholders.
* @param str The string that contains one or more placeholders.
* @param visited The placeholders that are considered visited (use for loop detection).
* @return The substituted string, if all placeholders have been successfully resolved.
* Returns null if str contains a single unresolved placeholder (to allow falling back to
* the default value).
* In case of partial success (some placeholder resolved) it returns the result using a empty
* string for all the unresolved ones.
*/
private String substitute(String str, Set<String> visited) {
String result = str;
Matcher matcher = PLACEHOLDER_PATTERN.matcher(str);
CopyOnWriteArraySet<String> copyOfVisited = new CopyOnWriteArraySet<>(visited);
while (matcher.find()) {
String name = matcher.group(1);
String replacement = null;
String toReplace = String.format(BOX_FORMAT, name);
for (PropertiesProvider provider : getDelegates()) {
if (provider.getProperty(name) != null && !visited.contains(name)) {
Object rawValue = provider.getProperty(name);
if (isCyclicReference(name, rawValue)) {
continue;
}
replacement = String.valueOf(rawValue);
if (PLACEHOLDER_PATTERN.matcher(replacement).matches()) {
copyOfVisited.add(name);
replacement = substitute(replacement, copyOfVisited);
}
}
}
if (replacement != null) {
result = result.replaceAll(toReplace, Matcher.quoteReplacement(replacement));
} else if (!str.equals(toReplace.replace("\\",""))) {
result = result.replaceAll(toReplace, "");
} else {
result = null;
}
}
return result;
}
/**
* Checks if the value is a placeholder reference to the key.
* @param key The key.
* @param value The value.
* @return True if a cycle is detected or false.
*/
private static boolean isCyclicReference(String key, Object value) {
if (!(value instanceof String)) {
return false;
} else return String.format(UNESCAPED_BOX_FORMAT, key).equals(value);
}
}