001/*
002 * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo team
018 *     Kevin Leturc <kleturc@nuxeo.com>
019 */
020package org.nuxeo.launcher.config;
021
022import java.io.File;
023import java.io.IOException;
024import java.net.MalformedURLException;
025import java.net.URL;
026import java.net.URLClassLoader;
027import java.nio.file.FileSystems;
028import java.nio.file.Path;
029import java.nio.file.PathMatcher;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.Collections;
034import java.util.HashSet;
035import java.util.List;
036import java.util.Optional;
037import java.util.Properties;
038import java.util.Set;
039import java.util.concurrent.TimeUnit;
040
041import org.apache.commons.lang3.StringUtils;
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.nuxeo.common.utils.TextTemplate;
045import org.nuxeo.launcher.config.backingservices.BackingChecker;
046
047import net.jodah.failsafe.Failsafe;
048import net.jodah.failsafe.FailsafeException;
049import net.jodah.failsafe.RetryPolicy;
050
051/**
052 * Calls backing services checks to verify that they are ready to use before starting Nuxeo.
053 *
054 * @since 9.2
055 */
056public class BackingServiceConfigurator {
057
058    protected static final Log log = LogFactory.getLog(BackingServiceConfigurator.class);
059
060    public static final String PARAM_RETRY_POLICY_ENABLED = "nuxeo.backing.check.retry.enabled";
061
062    public static final String PARAM_RETRY_POLICY_MAX_RETRIES = "nuxeo.backing.check.retry.maxRetries";
063
064    public static final String PARAM_RETRY_POLICY_DELAY_IN_MS = "nuxeo.backing.check.retry.delayInMs";
065
066    public static final String PARAM_POLICY_DEFAULT_DELAY_IN_MS = "5000";
067
068    public static final String PARAM_RETRY_POLICY_DEFAULT_RETRIES = "20";
069
070    public static final String PARAM_CHECK_CLASSPATH_SUFFIX = ".check.classpath";
071
072    public static final String PARAM_CHECK_SUFFIX = ".check.class";
073
074    protected static final String JAR_EXTENSION = ".jar";
075
076    protected Set<BackingChecker> checkers;
077
078    protected ConfigurationGenerator configurationGenerator;
079
080    public BackingServiceConfigurator(ConfigurationGenerator configurationGenerator) {
081        this.configurationGenerator = configurationGenerator;
082    }
083
084    /**
085     * Calls all BackingChecker if they accept the current configuration.
086     *
087     * @throws ConfigurationException
088     */
089    public void verifyInstallation() throws ConfigurationException {
090
091        RetryPolicy retryPolicy = buildRetryPolicy();
092
093        // Get all checkers
094        for (BackingChecker checker : getCheckers()) {
095            if (checker.accepts(configurationGenerator)) {
096                try {
097                    Failsafe.with(retryPolicy)
098                            .onFailedAttempt(failure -> log.error(failure.getMessage())) //
099                            .onRetry((c, f,
100                                    ctx) -> log.warn(String.format("Failure %d. Retrying....", ctx.getExecutions()))) //
101                            .run(() -> checker.check(configurationGenerator)); //
102                } catch (FailsafeException e) {
103                    if (e.getCause() instanceof ConfigurationException) {
104                        throw ((ConfigurationException) e.getCause());
105                    } else {
106                        throw e;
107                    }
108                }
109            }
110        }
111    }
112
113    protected RetryPolicy buildRetryPolicy() {
114        RetryPolicy retryPolicy = new RetryPolicy().withMaxRetries(0);
115
116        Properties userConfig = configurationGenerator.getUserConfig();
117        if (Boolean.parseBoolean((userConfig.getProperty(PARAM_RETRY_POLICY_ENABLED, "false")))) {
118
119            int maxRetries = Integer.parseInt(
120                    userConfig.getProperty(PARAM_RETRY_POLICY_MAX_RETRIES, PARAM_RETRY_POLICY_DEFAULT_RETRIES));
121            int delay = Integer.parseInt(
122                    userConfig.getProperty(PARAM_RETRY_POLICY_DELAY_IN_MS, PARAM_POLICY_DEFAULT_DELAY_IN_MS));
123
124            retryPolicy = retryPolicy.retryOn(ConfigurationException.class).withMaxRetries(maxRetries).withDelay(delay,
125                    TimeUnit.MILLISECONDS);
126        }
127        return retryPolicy;
128    }
129
130    protected Collection<BackingChecker> getCheckers() throws ConfigurationException {
131
132        if (checkers == null) {
133            checkers = new HashSet<>();
134
135            for (String template : configurationGenerator.getTemplateList()) {
136                try {
137                    File templateDir = configurationGenerator.getTemplateConf(template).getParentFile();
138                    String classPath = getClasspathForTemplate(template);
139                    String checkClass = configurationGenerator.getUserConfig()
140                                                              .getProperty(template + PARAM_CHECK_SUFFIX);
141
142                    Optional<URLClassLoader> ucl = getClassLoaderForTemplate(templateDir, classPath);
143                    if (ucl.isPresent()) {
144                        Class<?> klass = Class.forName(checkClass, true, ucl.get());
145                        checkers.add((BackingChecker) klass.newInstance());
146                    }
147
148                } catch (IOException e) {
149                    log.warn("Unable to read check configuration for template : " + template, e);
150                } catch (ReflectiveOperationException | ClassCastException e) {
151                    throw new ConfigurationException("Unable to check configuration for backing service " + template,
152                            e);
153                }
154            }
155        }
156        return checkers;
157    }
158
159    /**
160     * Read the classpath parameter from the template and expand parameters with their value. It allow classpath of the
161     * form ${nuxeo.home}/nxserver/bundles/...
162     *
163     * @param template The name of the template
164     */
165    // VisibleForTesting
166    String getClasspathForTemplate(String template) {
167        String classPath = configurationGenerator.getUserConfig().getProperty(template + PARAM_CHECK_CLASSPATH_SUFFIX);
168        TextTemplate templateParser = new TextTemplate(configurationGenerator.getUserConfig());
169        return templateParser.processText(classPath);
170    }
171
172    /**
173     * Build a ClassLoader based on the classpath definition of a template.
174     *
175     * @since 9.2
176     */
177    protected Optional<URLClassLoader> getClassLoaderForTemplate(File templateDir, String classPath)
178            throws ConfigurationException, IOException {
179        if (StringUtils.isBlank(classPath)) {
180            return Optional.empty();
181        }
182
183        String[] classpathEntries = classPath.split(":");
184
185        List<URL> urlsList = new ArrayList<>();
186
187        List<File> files = new ArrayList<>();
188        for (String entry : classpathEntries) {
189            files.addAll(getJarsFromClasspathEntry(templateDir.toPath(), entry));
190        }
191
192        if (!files.isEmpty()) {
193            for (File file : files) {
194                try {
195                    urlsList.add(new URL("jar:file:" + file.getPath() + "!/"));
196                    log.debug("Added " + file.getPath());
197                } catch (MalformedURLException e) {
198                    log.error(e);
199                }
200            }
201        } else {
202            return Optional.empty();
203        }
204
205        URLClassLoader ucl = new URLClassLoader(urlsList.toArray(new URL[0]));
206        return Optional.of(ucl);
207    }
208
209    /**
210     * Given a single classpath entry, return the liste of JARs referenced by it.<br>
211     * For instance :
212     * <ul>
213     * <li>nxserver/lib -> ${templatePath}/nxserver/lib</li>
214     * <li>/somePath/someLib-*.jar</li>
215     * </ul>
216     */
217    // VisibleForTesting
218    Collection<File> getJarsFromClasspathEntry(Path templatePath, String entry) {
219
220        Collection<File> jars = new ArrayList<>();
221
222        // Source path are expressed with "/", so we convert them to the current FS impl.
223        entry = entry.replace("/", File.separator);
224
225        // Add templatePath if relative classPath
226        String path = new File(entry).isAbsolute() ? entry : templatePath.toString() + File.separator + entry;
227
228        int slashIndex = path.lastIndexOf(File.separator);
229        if (slashIndex == -1) {
230            return Collections.emptyList();
231        }
232
233        String dirName = path.substring(0, slashIndex);
234        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + path);
235
236        File parentDir = new File(dirName);
237        File[] realMatchingFiles = parentDir.listFiles(f -> matcher.matches(f.toPath())
238                && f.toPath().startsWith(configurationGenerator.getNuxeoHome().toPath()));
239
240        if (realMatchingFiles != null) {
241            for (File file : realMatchingFiles) {
242                if (file.isDirectory()) {
243                    jars.addAll(Arrays.asList(file.listFiles(f -> f.getName().endsWith(JAR_EXTENSION))));
244                } else {
245                    if (file.getName().endsWith(JAR_EXTENSION)) {
246                        jars.add(file);
247                    }
248                }
249            }
250        }
251        return jars;
252    }
253}