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.logging.log4j.LogManager;
043import org.apache.logging.log4j.Logger;
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 Logger log = LogManager.getLogger(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    public void verifyInstallation() throws ConfigurationException {
088
089        RetryPolicy retryPolicy = buildRetryPolicy();
090
091        // Get all checkers
092        for (BackingChecker checker : getCheckers()) {
093            if (checker.accepts(configurationGenerator)) {
094                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
095                try {
096                    // Propagate the checker's class loader for jaas
097                    Thread.currentThread().setContextClassLoader(checker.getClass().getClassLoader());
098                    Failsafe.with(retryPolicy)
099                            .onFailedAttempt(failure -> log.error(failure.getMessage(), failure)) //
100                            .onRetry((c, f,
101                                    ctx) -> log.warn(String.format("Failure %d. Retrying....", ctx.getExecutions()))) //
102                            .run(() -> checker.check(configurationGenerator)); //
103                } catch (FailsafeException e) {
104                    if (e.getCause() instanceof ConfigurationException) {
105                        throw ((ConfigurationException) e.getCause());
106                    } else {
107                        throw e;
108                    }
109                } finally {
110                    Thread.currentThread().setContextClassLoader(classLoader);
111                }
112
113            }
114        }
115    }
116
117    protected RetryPolicy buildRetryPolicy() {
118        RetryPolicy retryPolicy = new RetryPolicy().withMaxRetries(0);
119
120        Properties userConfig = configurationGenerator.getUserConfig();
121        if (Boolean.parseBoolean((userConfig.getProperty(PARAM_RETRY_POLICY_ENABLED, "false")))) {
122
123            int maxRetries = Integer.parseInt(
124                    userConfig.getProperty(PARAM_RETRY_POLICY_MAX_RETRIES, PARAM_RETRY_POLICY_DEFAULT_RETRIES));
125            int delay = Integer.parseInt(
126                    userConfig.getProperty(PARAM_RETRY_POLICY_DELAY_IN_MS, PARAM_POLICY_DEFAULT_DELAY_IN_MS));
127
128            retryPolicy = retryPolicy.retryOn(ConfigurationException.class)
129                                     .withMaxRetries(maxRetries)
130                                     .withDelay(delay, TimeUnit.MILLISECONDS);
131        }
132        return retryPolicy;
133    }
134
135    protected Collection<BackingChecker> getCheckers() throws ConfigurationException {
136
137        if (checkers == null) {
138            checkers = new HashSet<>();
139            List<String> items = configurationGenerator.getTemplateList();
140            // Add backing without template
141            items.add("elasticsearch");
142            items.add("kafka");
143            for (String item : items) {
144                try {
145                    log.debug("checker: {}", item);
146                    File templateDir = getTemplateDir(item);
147                    String classPath = getClasspathForTemplate(item);
148                    String checkClass = configurationGenerator.getUserConfig()
149                                                              .getProperty(item + PARAM_CHECK_SUFFIX);
150                    Optional<URLClassLoader> ucl = getClassLoaderForTemplate(templateDir, classPath);
151                    if (ucl.isPresent()) {
152                        Class<?> klass = Class.forName(checkClass, true, ucl.get());
153                        log.debug("Adding checker: {} with class path: {}", () -> item,
154                                () -> Arrays.toString(ucl.get().getURLs()));
155                        checkers.add((BackingChecker) klass.getDeclaredConstructor().newInstance());
156                    }
157                } catch (IOException e) {
158                    log.warn("Unable to read check configuration for template: {}", item, e);
159                } catch (ReflectiveOperationException | ClassCastException e) {
160                    throw new ConfigurationException("Unable to check configuration for backing service " + item,
161                            e);
162                }
163            }
164        }
165        return checkers;
166    }
167
168    protected File getTemplateDir(String item) throws ConfigurationException {
169        try {
170            return configurationGenerator.getTemplateDirectory(item);
171        } catch (ConfigurationException e) {
172            return configurationGenerator.getTemplateDirectory("default");
173        }
174    }
175
176    /**
177     * Read the classpath parameter from the template and expand parameters with their value. It allow classpath of the
178     * form ${nuxeo.home}/nxserver/bundles/...
179     *
180     * @param template The name of the template
181     */
182    // VisibleForTesting
183    String getClasspathForTemplate(String template) {
184        String classPath = configurationGenerator.getUserConfig().getProperty(template + PARAM_CHECK_CLASSPATH_SUFFIX);
185        TextTemplate templateParser = new TextTemplate(configurationGenerator.getUserConfig());
186        String result = templateParser.processText(classPath);
187        return result == null ? null : result.replace("/", File.separator);
188    }
189
190    /**
191     * Build a ClassLoader based on the classpath definition of a template.
192     *
193     * @since 9.2
194     */
195    protected Optional<URLClassLoader> getClassLoaderForTemplate(File templateDir, String classPathEntry)
196            throws ConfigurationException, IOException {
197        if (StringUtils.isBlank(classPathEntry)) {
198            return Optional.empty();
199        }
200
201        String[] classpathEntries = classPathEntry.split(":");
202
203        List<URL> urlsList = new ArrayList<>();
204
205        List<File> files = new ArrayList<>();
206        for (String entry : classpathEntries) {
207            files.addAll(getJarsFromClasspathEntry(templateDir.toPath(), entry));
208        }
209
210        if (!files.isEmpty()) {
211            for (File file : files) {
212                try {
213                    urlsList.add(new URL("jar:file:" + file.getPath() + "!/"));
214                    log.debug("Adding url: {}", file.getPath());
215                } catch (MalformedURLException e) {
216                    log.error(e);
217                }
218            }
219        } else {
220            return Optional.empty();
221        }
222
223        URLClassLoader ucl = new URLClassLoader(urlsList.toArray(new URL[0]));
224        return Optional.of(ucl);
225    }
226
227    /**
228     * Given a single classpath entry, return the liste of JARs referenced by it.<br>
229     * For instance :
230     * <ul>
231     * <li>nxserver/lib -> ${templatePath}/nxserver/lib</li>
232     * <li>/somePath/someLib-*.jar</li>
233     * </ul>
234     */
235    // VisibleForTesting
236    Collection<File> getJarsFromClasspathEntry(Path templatePath, String entry) {
237
238        Collection<File> jars = new ArrayList<>();
239
240        // Source path are expressed with "/", so we convert them to the current FS impl.
241        entry = entry.replace("/", File.separator);
242
243        // Add templatePath if relative classPath
244        String path = new File(entry).isAbsolute() ? entry : templatePath.toString() + File.separator + entry;
245
246        int slashIndex = path.lastIndexOf(File.separator);
247        if (slashIndex == -1) {
248            return Collections.emptyList();
249        }
250
251        String dirName = path.substring(0, slashIndex);
252        // ugly trick mandatory to let the PathMatcher match on windows
253        path = path.replaceAll("\\\\", "\\\\\\\\");
254        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + path);
255
256        File parentDir = new File(dirName);
257        File[] realMatchingFiles = parentDir.listFiles(f -> matcher.matches(f.toPath())
258                && f.toPath().startsWith(configurationGenerator.getNuxeoHome().toPath()));
259
260        if (realMatchingFiles != null) {
261            for (File file : realMatchingFiles) {
262                if (file.isDirectory()) {
263                    jars.addAll(Arrays.asList(file.listFiles(f -> f.getName().endsWith(JAR_EXTENSION))));
264                } else {
265                    if (file.getName().endsWith(JAR_EXTENSION)) {
266                        jars.add(file);
267                    }
268                }
269            }
270        }
271        return jars;
272    }
273}