001/* 002 * (C) Copyright 2006-2018 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 - initial API and implementation 018 */ 019package org.nuxeo.runtime.test; 020 021import java.io.File; 022import java.io.IOException; 023import java.net.MalformedURLException; 024import java.net.URI; 025import java.net.URISyntaxException; 026import java.net.URL; 027import java.nio.file.Files; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.Objects; 035import java.util.Properties; 036import java.util.Set; 037import java.util.jar.Attributes; 038import java.util.jar.Manifest; 039import java.util.stream.Collectors; 040import java.util.stream.Stream; 041 042import org.apache.commons.io.FileUtils; 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.common.Environment; 046import org.nuxeo.osgi.BundleFile; 047import org.nuxeo.osgi.BundleImpl; 048import org.nuxeo.osgi.DirectoryBundleFile; 049import org.nuxeo.osgi.JarBundleFile; 050import org.nuxeo.osgi.OSGiAdapter; 051import org.nuxeo.osgi.SystemBundle; 052import org.nuxeo.osgi.SystemBundleFile; 053import org.nuxeo.osgi.application.StandaloneBundleLoader; 054import org.nuxeo.runtime.RuntimeServiceException; 055import org.nuxeo.runtime.api.Framework; 056import org.nuxeo.runtime.model.Extension; 057import org.nuxeo.runtime.model.RegistrationInfo; 058import org.nuxeo.runtime.model.RuntimeContext; 059import org.nuxeo.runtime.model.StreamRef; 060import org.nuxeo.runtime.model.URLStreamRef; 061import org.nuxeo.runtime.model.impl.DefaultRuntimeContext; 062import org.nuxeo.runtime.osgi.OSGiRuntimeContext; 063import org.nuxeo.runtime.osgi.OSGiRuntimeService; 064import org.nuxeo.runtime.test.protocols.inline.InlineURLFactory; 065import org.nuxeo.runtime.test.runner.RuntimeHarness; 066import org.nuxeo.runtime.test.runner.TargetExtensions; 067import org.nuxeo.runtime.transaction.TransactionHelper; 068import org.osgi.framework.Bundle; 069import org.osgi.framework.FrameworkEvent; 070 071import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; 072 073/** 074 * Default RuntimeHarness implementation. 075 * 076 * @since 10.2 077 */ 078public class RuntimeHarnessImpl implements RuntimeHarness { 079 080 protected static final Log log = LogFactory.getLog(RuntimeHarnessImpl.class); 081 082 protected static URL[] introspectClasspath() { 083 return new FastClasspathScanner().getUniqueClasspathElements().stream().map(file -> { 084 try { 085 return file.toURI().toURL(); 086 } catch (MalformedURLException cause) { 087 throw new RuntimeServiceException("Could not get URL from " + file, cause); 088 } 089 }).toArray(URL[]::new); 090 } 091 092 protected StandaloneBundleLoader bundleLoader; 093 094 protected Map<String, BundleFile> bundles; 095 096 protected boolean frameworkStarted; 097 098 protected OSGiAdapter osgi; 099 100 protected Set<URI> readUris; 101 102 protected OSGiRuntimeService runtime; 103 104 protected Bundle runtimeBundle; 105 106 protected TargetResourceLocator targetResourceLocator; 107 108 protected URL[] urls; // classpath urls, used for bundles lookup 109 110 protected List<WorkingDirectoryConfigurator> wdConfigs; 111 112 protected File workingDir; 113 114 protected RuntimeHarnessImpl() { 115 wdConfigs = new ArrayList<>(); 116 } 117 118 public RuntimeHarnessImpl(Class<?> clazz) { 119 this(); 120 targetResourceLocator = new TargetResourceLocator(clazz); 121 } 122 123 @Override 124 public void addWorkingDirectoryConfigurator(WorkingDirectoryConfigurator config) { 125 wdConfigs.add(config); 126 } 127 128 @Override 129 public void deployBundle(String name) throws Exception { 130 // install only if not yet installed 131 Bundle bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); 132 if (bundle == null) { 133 BundleFile bundleFile = lookupBundle(name); 134 bundleLoader.loadBundle(bundleFile); 135 bundleLoader.installBundle(bundleFile); 136 bundle = bundleLoader.getOSGi().getRegistry().getBundle(name); 137 } else { 138 log.info(String.format("A bundle with name %s has been found. Deploy is ignored.", name)); 139 } 140 if (runtime.getContext(bundle) == null) { 141 runtime.createContext(bundle); 142 } 143 } 144 145 @Override 146 public void deployContrib(String name, String contrib) throws Exception { 147 RuntimeContext context = runtime.getContext(name); 148 if (context == null) { 149 context = runtime.getContext(); 150 BundleFile file = lookupBundle(name); 151 URL location = file.getEntry(contrib); 152 if (location == null) { 153 throw new AssertionError("Cannot locate " + contrib + " in " + name); 154 } 155 context.deploy(location); 156 } else { 157 context.deploy(contrib); 158 } 159 } 160 161 @Override 162 @Deprecated 163 public void deployFolder(File folder, ClassLoader loader) throws Exception { 164 DirectoryBundleFile bf = new DirectoryBundleFile(folder); 165 BundleImpl bundle = new BundleImpl(osgi, bf, loader); 166 osgi.install(bundle); 167 } 168 169 @Override 170 public RuntimeContext deployPartial(String name, Set<TargetExtensions> targetExtensions) throws Exception { 171 // Do not install bundle; we only need the Object to list his components 172 Bundle bundle = new BundleImpl(osgi, lookupBundle(name), getClass().getClassLoader()); 173 RuntimeContext ctx = new OSGiRuntimeContext(runtime, bundle); 174 listBundleComponents(bundle).map(URLStreamRef::new).forEach(component -> { 175 try { 176 deployPartialComponent(ctx, targetExtensions, component); 177 } catch (IOException e) { 178 log.error("PartialBundle: " + name + " failed to load: " + component, e); 179 } 180 }); 181 return ctx; 182 } 183 184 @Override 185 @Deprecated 186 public RuntimeContext deployTestContrib(String bundle, String contrib) throws Exception { 187 URL url = targetResourceLocator.getTargetTestResource(contrib); 188 return deployTestContrib(bundle, url); 189 } 190 191 @Override 192 @Deprecated 193 public RuntimeContext deployTestContrib(String bundle, URL contrib) throws Exception { 194 Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle); 195 if (b == null) { 196 b = osgi.getSystemBundle(); 197 } 198 OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b); 199 ctx.deploy(contrib); 200 return ctx; 201 } 202 203 @Override 204 public void fireFrameworkStarted() throws Exception { 205 if (frameworkStarted) { 206 throw new IllegalStateException("fireFrameworkStarted must not be called more than once"); 207 } 208 frameworkStarted = true; 209 boolean txStarted = !TransactionHelper.isTransactionActiveOrMarkedRollback() 210 && TransactionHelper.startTransaction(); 211 boolean txFinished = false; 212 try { 213 osgi.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, runtimeBundle, null)); 214 txFinished = true; 215 } finally { 216 if (!txFinished) { 217 TransactionHelper.setTransactionRollbackOnly(); 218 } 219 if (txStarted) { 220 TransactionHelper.commitOrRollbackTransaction(); 221 } 222 } 223 } 224 225 @Override 226 @Deprecated 227 public List<String> getClassLoaderFiles() throws URISyntaxException { 228 List<String> files = new ArrayList<>(urls.length); 229 for (URL url : urls) { 230 files.add(url.toURI().getPath()); 231 } 232 return files; 233 } 234 235 @Override 236 public RuntimeContext getContext() { 237 return runtime.getContext(); 238 } 239 240 @Override 241 public OSGiAdapter getOSGiAdapter() { 242 return osgi; 243 } 244 245 @Override 246 @Deprecated 247 public Properties getProperties() { 248 return runtime.getProperties(); 249 } 250 251 @Override 252 public File getWorkingDir() { 253 return workingDir; 254 } 255 256 @Override 257 public boolean isRestart() { 258 return false; 259 } 260 261 @Override 262 public boolean isStarted() { 263 return runtime != null; 264 } 265 266 @Override 267 public void restart() throws Exception { 268 // do nothing 269 } 270 271 @Override 272 public void start() throws Exception { 273 System.setProperty("org.nuxeo.runtime.testing", "true"); 274 wipeRuntime(); 275 initUrls(); 276 if (urls == null) { 277 throw new UnsupportedOperationException("no bundles available"); 278 } 279 initOsgiRuntime(); 280 } 281 282 @Override 283 public void stop() throws Exception { 284 wipeRuntime(); 285 if (workingDir != null) { 286 if (workingDir.exists() && !FileUtils.deleteQuietly(workingDir)) { 287 log.warn("Cannot delete " + workingDir); 288 } 289 workingDir = null; 290 } 291 readUris = null; 292 bundles = null; 293 } 294 295 @Override 296 public void undeployContrib(String name, String contrib) throws Exception { 297 RuntimeContext context = runtime.getContext(name); 298 if (context == null) { 299 context = runtime.getContext(); 300 } 301 context.undeploy(contrib); 302 } 303 304 /** 305 * Read a component from his StreamRef and create a new component (suffixed with `-partial`, and the base component 306 * name aliased) with only matching contributions of the extensionPoints parameter. 307 * 308 * @param ctx RuntimeContext in which the new component will be deployed 309 * @param extensionPoints Set of white listed TargetExtensions 310 * @param component Reference to the original component 311 * @throws IOException Signals that an I/O exception has occurred. 312 */ 313 protected void deployPartialComponent(RuntimeContext ctx, Set<TargetExtensions> extensionPoints, 314 StreamRef component) throws IOException { 315 RegistrationInfo ri = ((DefaultRuntimeContext) ctx).createRegistrationInfo(component); 316 String name = ri.getName().getName() + "-partial"; 317 318 // Flatten Target Extension Points 319 Set<String> targets = extensionPoints.stream() 320 .map(TargetExtensions::getTargetExtensions) 321 .flatMap(Set::stream) 322 .collect(Collectors.toSet()); 323 324 String ext = Arrays.stream(ri.getExtensions()) 325 .filter(e -> targets.contains(TargetExtensions.newTargetExtension( 326 e.getTargetComponent().getName(), e.getExtensionPoint()))) 327 .map(Extension::toXML) 328 .collect(Collectors.joining()); 329 330 InlineURLFactory.install(); 331 ctx.deploy(new InlineRef(name, String.format("<component name=\"%s\">%s</component>", name, ext))); 332 } 333 334 /** 335 * Inits the osgi runtime. 336 * 337 * @throws Exception the exception 338 */ 339 protected void initOsgiRuntime() throws Exception { 340 try { 341 Environment.setDefault(null); 342 if (System.getProperties().remove("nuxeo.home") != null) { 343 log.warn("Removed System property nuxeo.home."); 344 } 345 workingDir = File.createTempFile("nxruntime-" + Thread.currentThread().getName() + "-", null, 346 new File("target")); 347 Files.delete(workingDir.toPath()); 348 } catch (IOException e) { 349 log.error("Could not init working directory", e); 350 throw e; 351 } 352 osgi = new OSGiAdapter(workingDir); 353 BundleFile bf = new SystemBundleFile(workingDir); 354 bundleLoader = new StandaloneBundleLoader(osgi, RuntimeHarnessImpl.class.getClassLoader()); 355 SystemBundle systemBundle = new SystemBundle(osgi, bf, bundleLoader.getSharedClassLoader().getLoader()); 356 osgi.setSystemBundle(systemBundle); 357 Thread.currentThread().setContextClassLoader(bundleLoader.getSharedClassLoader().getLoader()); 358 359 for (WorkingDirectoryConfigurator cfg : wdConfigs) { 360 cfg.configure(this, workingDir); 361 } 362 363 bundleLoader.setScanForNestedJARs(false); // for now 364 bundleLoader.setExtractNestedJARs(false); 365 366 BundleFile bundleFile = lookupBundle("org.nuxeo.runtime"); 367 runtimeBundle = new RootRuntimeBundle(osgi, bundleFile, bundleLoader.getClass().getClassLoader(), true); 368 runtimeBundle.start(); 369 370 runtime = (OSGiRuntimeService) Framework.getRuntime(); 371 372 } 373 374 /** 375 * Inits the urls. 376 * 377 * @throws Exception the exception 378 */ 379 protected void initUrls() throws Exception { 380 urls = introspectClasspath(); 381 if (log.isDebugEnabled()) { 382 StringBuilder sb = new StringBuilder(); 383 sb.append("URLs on the classpath: "); 384 for (URL url : urls) { 385 sb.append(url.toString()); 386 sb.append('\n'); 387 } 388 log.debug(sb.toString()); 389 } 390 readUris = new HashSet<>(); 391 bundles = new HashMap<>(); 392 } 393 394 /** 395 * Listing component's urls of a bundle. Inspired from org.nuxeo.runtime.osgi.OSGiRuntimeService#loadComponents but 396 * without deploying anything. 397 * 398 * @param bundle Bundle to be read 399 * @return the stream 400 */ 401 protected Stream<URL> listBundleComponents(Bundle bundle) { 402 String list = OSGiRuntimeService.getComponentsList(bundle); 403 String name = bundle.getSymbolicName(); 404 log.debug("PartialBundle: " + name + " components: " + list); 405 if (list == null) { 406 return Stream.empty(); 407 } else { 408 return Arrays.stream(list.split("[, \t\n\r\f]")).map(bundle::getEntry).filter(Objects::nonNull); 409 } 410 } 411 412 /** 413 * Lookup bundle. 414 * 415 * @param bundleName the bundle name 416 * @return the bundle file 417 * @throws Exception the exception 418 */ 419 protected BundleFile lookupBundle(String bundleName) throws Exception { 420 BundleFile bundleFile = bundles.get(bundleName); 421 if (bundleFile != null) { 422 return bundleFile; 423 } 424 for (URL url : urls) { 425 URI uri = url.toURI(); 426 if (readUris.contains(uri)) { 427 continue; 428 } 429 File file = new File(uri); 430 readUris.add(uri); 431 try { 432 if (file.isDirectory()) { 433 bundleFile = new DirectoryBundleFile(file); 434 } else { 435 bundleFile = new JarBundleFile(file); 436 } 437 } catch (IOException e) { 438 // no manifest => not a bundle 439 continue; 440 } 441 String symbolicName = readSymbolicName(bundleFile); 442 if (symbolicName != null) { 443 log.info(String.format("Bundle '%s' has URL %s", symbolicName, url)); 444 bundles.put(symbolicName, bundleFile); 445 } 446 if (bundleName.equals(symbolicName)) { 447 return bundleFile; 448 } 449 } 450 throw new RuntimeServiceException(String.format("No bundle with symbolic name '%s';", bundleName)); 451 } 452 453 /** 454 * Read symbolic name. 455 * 456 * @param bf the bf 457 * @return the string 458 */ 459 protected String readSymbolicName(BundleFile bf) { 460 Manifest manifest = bf.getManifest(); 461 if (manifest == null) { 462 return null; 463 } 464 Attributes attrs = manifest.getMainAttributes(); 465 String name = attrs.getValue("Bundle-SymbolicName"); 466 if (name == null) { 467 return null; 468 } 469 String[] sp = name.split(";", 2); 470 return sp[0]; 471 } 472 473 /** 474 * Makes sure there is no previous runtime hanging around. 475 * <p> 476 * This happens for instance if a previous test had errors in its <code>setUp()</code>, because 477 * <code>tearDown()</code> has not been called. 478 * 479 * @throws Exception the exception 480 */ 481 protected void wipeRuntime() throws Exception { 482 // Make sure there is no active runtime (this might happen if an 483 // exception is raised during a previous setUp -> tearDown is not called afterwards). 484 runtime = null; 485 frameworkStarted = false; 486 if (Framework.getRuntime() != null) { 487 try { 488 Framework.shutdown(); 489 } catch (InterruptedException cause) { 490 Thread.currentThread().interrupt(); 491 throw new RuntimeServiceException("Interrupted during shutdown", cause); 492 } 493 } 494 } 495 496}