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: Nuxeo team
017 *
018 */
019package org.nuxeo.launcher.config;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.net.URLClassLoader;
026import java.nio.file.FileSystems;
027import java.nio.file.Path;
028import java.nio.file.PathMatcher;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashSet;
034import java.util.List;
035import java.util.Optional;
036import java.util.Properties;
037import java.util.Set;
038import java.util.concurrent.TimeUnit;
039
040import org.apache.commons.lang.StringUtils;
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.common.utils.TextTemplate;
044import org.nuxeo.launcher.config.backingservices.BackingChecker;
045import org.nuxeo.launcher.config.backingservices.DBCheck;
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            checkers.add(new DBCheck());
156        }
157        return checkers;
158    }
159
160    /**
161     * Read the classpath parameter from the template and expand parameters with their value. It allow classpath of the
162     * form ${nuxeo.home}/nxserver/bundles/...
163     *
164     * @param template The name of the template
165     * @return
166     */
167    // VisibleForTesting
168    String getClasspathForTemplate(String template) {
169        String classPath = configurationGenerator.getUserConfig().getProperty(template + PARAM_CHECK_CLASSPATH_SUFFIX);
170        TextTemplate templateParser = new TextTemplate(configurationGenerator.getUserConfig());
171        return templateParser.processText(classPath);
172    }
173
174    /**
175     * Build a ClassLoader based on the classpath definition of a template.
176     *
177     * @since 9.2
178     */
179    protected Optional<URLClassLoader> getClassLoaderForTemplate(File templateDir, String classPath)
180            throws ConfigurationException, IOException {
181        if (StringUtils.isBlank(classPath)) {
182            return Optional.empty();
183        }
184
185        String[] classpathEntries = classPath.split(":");
186
187        List<URL> urlsList = new ArrayList<>();
188
189        List<File> files = new ArrayList<>();
190        for (String entry : classpathEntries) {
191            files.addAll(getJarsFromClasspathEntry(templateDir.toPath(), entry));
192        }
193
194        if (!files.isEmpty()) {
195            for (File file : files) {
196                try {
197                    urlsList.add(new URL("jar:file:" + file.getPath() + "!/"));
198                    log.debug("Added " + file.getPath());
199                } catch (MalformedURLException e) {
200                    log.error(e);
201                }
202            }
203        } else {
204            return Optional.empty();
205        }
206
207        URLClassLoader ucl = new URLClassLoader(urlsList.toArray(new URL[0]));
208        return Optional.of(ucl);
209    }
210
211    /**
212     * Given a single classpath entry, return the liste of JARs referenced by it.<br>
213     * For instance :
214     * <ul>
215     * <li>nxserver/lib -> ${templatePath}/nxserver/lib</li>
216     * <li>/somePath/someLib-*.jar</li>
217     * </ul>
218     */
219    // VisibleForTesting
220    Collection<File> getJarsFromClasspathEntry(Path templatePath, String entry) {
221
222        Collection<File> jars = new ArrayList<>();
223
224        // Source path are expressed with "/", so we convert them to the current FS impl.
225        entry = entry.replace("/", File.separator);
226
227        // Add templatePath if relative classPath
228        String path = new File(entry).isAbsolute() ? entry : templatePath.toString() + File.separator + entry;
229
230        int slashIndex = path.lastIndexOf(File.separator);
231        if (slashIndex == -1) {
232            return Collections.emptyList();
233        }
234
235        String dirName = path.substring(0, slashIndex);
236        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + path);
237
238        File parentDir = new File(dirName);
239        File[] realMatchingFiles = parentDir.listFiles(f -> matcher.matches(f.toPath())
240                && f.toPath().startsWith(configurationGenerator.getNuxeoHome().toPath()));
241
242        if (realMatchingFiles != null) {
243            for (File file : realMatchingFiles) {
244                if (file.isDirectory()) {
245                    jars.addAll(Arrays.asList(file.listFiles(f -> f.getName().endsWith(JAR_EXTENSION))));
246                } else {
247                    if (file.getName().endsWith(JAR_EXTENSION)) {
248                        jars.add(file);
249                    }
250                }
251            }
252        }
253        return jars;
254    }
255}