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}