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 <[email protected]> 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}