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}