/*
* (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors: Nuxeo team
*
*/
package org.nuxeo.launcher.config;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.TextTemplate;
import org.nuxeo.launcher.config.backingservices.BackingChecker;
import org.nuxeo.launcher.config.backingservices.DBCheck;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.FailsafeException;
import net.jodah.failsafe.RetryPolicy;
/**
* Calls backing services checks to verify that they are ready to use before starting Nuxeo.
*
* @since 9.2
*/
public class BackingServiceConfigurator {
protected static final Log log = LogFactory.getLog(BackingServiceConfigurator.class);
public static final String PARAM_RETRY_POLICY_ENABLED = "nuxeo.backing.check.retry.enabled";
public static final String PARAM_RETRY_POLICY_MAX_RETRIES = "nuxeo.backing.check.retry.maxRetries";
public static final String PARAM_RETRY_POLICY_DELAY_IN_MS = "nuxeo.backing.check.retry.delayInMs";
public static final String PARAM_POLICY_DEFAULT_DELAY_IN_MS = "5000";
public static final String PARAM_RETRY_POLICY_DEFAULT_RETRIES = "20";
public static final String PARAM_CHECK_CLASSPATH_SUFFIX = ".check.classpath";
public static final String PARAM_CHECK_SUFFIX = ".check.class";
protected static final String JAR_EXTENSION = ".jar";
protected Set<BackingChecker> checkers;
protected ConfigurationGenerator configurationGenerator;
public BackingServiceConfigurator(ConfigurationGenerator configurationGenerator) {
this.configurationGenerator = configurationGenerator;
}
/**
* Calls all BackingChecker if they accept the current configuration.
*
* @throws ConfigurationException
*/
public void verifyInstallation() throws ConfigurationException {
RetryPolicy retryPolicy = buildRetryPolicy();
// Get all checkers
for (BackingChecker checker : getCheckers()) {
if (checker.accepts(configurationGenerator)) {
try {
Failsafe.with(retryPolicy)
.onFailedAttempt(failure -> log.error(failure.getMessage())) //
.onRetry((c, f,
ctx) -> log.warn(String.format("Failure %d. Retrying....", ctx.getExecutions()))) //
.run(() -> checker.check(configurationGenerator)); //
} catch (FailsafeException e) {
if (e.getCause() instanceof ConfigurationException) {
throw ((ConfigurationException) e.getCause());
} else {
throw e;
}
}
}
}
}
protected RetryPolicy buildRetryPolicy() {
RetryPolicy retryPolicy = new RetryPolicy().withMaxRetries(0);
Properties userConfig = configurationGenerator.getUserConfig();
if (Boolean.parseBoolean((userConfig.getProperty(PARAM_RETRY_POLICY_ENABLED, "false")))) {
int maxRetries = Integer.parseInt(
userConfig.getProperty(PARAM_RETRY_POLICY_MAX_RETRIES, PARAM_RETRY_POLICY_DEFAULT_RETRIES));
int delay = Integer.parseInt(
userConfig.getProperty(PARAM_RETRY_POLICY_DELAY_IN_MS, PARAM_POLICY_DEFAULT_DELAY_IN_MS));
retryPolicy = retryPolicy.retryOn(ConfigurationException.class).withMaxRetries(maxRetries).withDelay(delay,
TimeUnit.MILLISECONDS);
}
return retryPolicy;
}
protected Collection<BackingChecker> getCheckers() throws ConfigurationException {
if (checkers == null) {
checkers = new HashSet<>();
for (String template : configurationGenerator.getTemplateList()) {
try {
File templateDir = configurationGenerator.getTemplateConf(template).getParentFile();
String classPath = getClasspathForTemplate(template);
String checkClass = configurationGenerator.getUserConfig().getProperty(template + PARAM_CHECK_SUFFIX);
Optional<URLClassLoader> ucl = getClassLoaderForTemplate(templateDir, classPath);
if (ucl.isPresent()) {
Class<?> klass = Class.forName(checkClass, true, ucl.get());
checkers.add((BackingChecker) klass.newInstance());
}
} catch (IOException e) {
log.warn("Unable to read check configuration for template : " + template, e);
} catch (ReflectiveOperationException | ClassCastException e) {
throw new ConfigurationException("Unable to check configuration for backing service " + template,
e);
}
}
checkers.add(new DBCheck());
}
return checkers;
}
/**
* Read the classpath parameter from the template and expand parameters with their value. It allow classpath of the
* form ${nuxeo.home}/nxserver/bundles/...
*
* @param template The name of the template
* @return
*/
//VisibleForTesting
String getClasspathForTemplate(String template) {
String classPath = configurationGenerator.getUserConfig().getProperty(template + PARAM_CHECK_CLASSPATH_SUFFIX);
TextTemplate templateParser = new TextTemplate(configurationGenerator.getUserConfig());
return templateParser.processText(classPath);
}
/**
* Build a ClassLoader based on the classpath definition of a template.
*
* @since 9.2
*/
protected Optional<URLClassLoader> getClassLoaderForTemplate(File templateDir, String classPath)
throws ConfigurationException, IOException {
if (StringUtils.isBlank(classPath)) {
return Optional.empty();
}
String[] classpathEntries = classPath.split(":");
List<URL> urlsList = new ArrayList<>();
List<File> files = new ArrayList<>();
for (String entry : classpathEntries) {
files.addAll(getJarsFromClasspathEntry(templateDir.toPath(), entry));
}
if (!files.isEmpty()) {
for (File file : files) {
try {
urlsList.add(new URL("jar:file:" + file.getPath() + "!/"));
log.debug("Added " + file.getPath());
} catch (MalformedURLException e) {
log.error(e);
}
}
} else {
return Optional.empty();
}
URLClassLoader ucl = new URLClassLoader(urlsList.toArray(new URL[0]));
return Optional.of(ucl);
}
/**
* Given a single classpath entry, return the liste of JARs referenced by it.<br>
* For instance :
* <ul>
* <li>nxserver/lib -> ${templatePath}/nxserver/lib</li>
* <li>/somePath/someLib-*.jar</li>
* </ul>
*
*/
// VisibleForTesting
Collection<File> getJarsFromClasspathEntry(Path templatePath, String entry) {
Collection<File> jars = new ArrayList<>();
// Add templatePath if relative classPath
Path target = entry.startsWith("/") ? Paths.get(entry) : Paths.get(templatePath.toString(), entry);
String path = target.toString();
int slashIndex = path.lastIndexOf("/");
String dirName = path.substring(0, slashIndex);
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + path);
File parentDir = new File(dirName);
File[] realMatchingFiles = parentDir.listFiles(
f -> matcher.matches(f.toPath()) && f.toPath().startsWith(configurationGenerator.getNuxeoHome().toPath()));
if (realMatchingFiles != null) {
for (File file : realMatchingFiles) {
if (file.isDirectory()) {
jars.addAll(Arrays.asList(file.listFiles(f -> f.getName().endsWith(JAR_EXTENSION))));
} else {
if (file.getName().endsWith(JAR_EXTENSION)) {
jars.add(file);
}
}
}
}
return jars;
}
}