001package org.apache.turbine.services; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022 023import java.util.ArrayList; 024import java.util.Iterator; 025import java.util.LinkedHashMap; 026import java.util.LinkedHashSet; 027import java.util.Map; 028import java.util.Set; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.concurrent.locks.ReentrantLock; 031 032import org.apache.commons.configuration2.Configuration; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.logging.log4j.LogManager; 035import org.apache.logging.log4j.Logger; 036 037/** 038 * A generic implementation of a <code>ServiceBroker</code> which 039 * provides: 040 * 041 * <ul> 042 * <li>Maintaining service name to class name mapping, allowing 043 * pluggable service implementations.</li> 044 * <li>Providing <code>Services</code> with a configuration based on 045 * system wide configuration mechanism.</li> 046 * <li>Integration of TurbineServiceProviders for looking up 047 * non-local services</li> 048 * </ul> 049 * 050 * @author <a href="mailto:burton@apache.org">Kevin Burton</a> 051 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a> 052 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a> 053 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> 054 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a> 055 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 056 * @version $Id$ 057 */ 058public abstract class BaseServiceBroker implements ServiceBroker 059{ 060 /** 061 * Mapping of Service names to class names, keep order. 062 */ 063 private final Map<String, Class<?>> mapping = new LinkedHashMap<>(); 064 065 /** 066 * A repository of Service instances. 067 */ 068 private final ConcurrentHashMap<String, Service> services = new ConcurrentHashMap<>(); 069 070 /** 071 * Lock access during service initialization 072 */ 073 private final ReentrantLock serviceLock = new ReentrantLock(); 074 075 /** 076 * Configuration for the services broker. 077 * The configuration should be set by the application 078 * in which the services framework is running. 079 */ 080 private Configuration configuration; 081 082 /** 083 * A prefix for <code>Service</code> properties in 084 * TurbineResource.properties. 085 */ 086 public static final String SERVICE_PREFIX = "services."; 087 088 /** 089 * A <code>Service</code> property determining its implementing 090 * class name . 091 */ 092 public static final String CLASSNAME_SUFFIX = ".classname"; 093 094 /** 095 * These are objects that the parent application 096 * can provide so that application specific 097 * services have a mechanism to retrieve specialized 098 * information. For example, in Turbine there are services 099 * that require the RunData object: these services can 100 * retrieve the RunData object that Turbine has placed 101 * in the service manager. This alleviates us of 102 * the requirement of having init(Object) all 103 * together. 104 */ 105 private final ConcurrentHashMap<String, Object> serviceObjects = new ConcurrentHashMap<>(); 106 107 /** Logging */ 108 private static final Logger log = LogManager.getLogger(BaseServiceBroker.class); 109 110 /** 111 * Application root path as set by the 112 * parent application. 113 */ 114 private String applicationRoot; 115 116 /** 117 * mapping from service names to instances of TurbineServiceProviders 118 */ 119 private final ConcurrentHashMap<String, Service> serviceProviderInstanceMap = new ConcurrentHashMap<>(); 120 121 /** 122 * Default constructor, protected as to only be usable by subclasses. 123 * 124 * This constructor does nothing. 125 */ 126 protected BaseServiceBroker() 127 { 128 // nothing to do 129 } 130 131 /** 132 * Set the configuration object for the services broker. 133 * This is the configuration that contains information 134 * about all services in the care of this service 135 * manager. 136 * 137 * @param configuration Broker configuration. 138 */ 139 public void setConfiguration(Configuration configuration) 140 { 141 this.configuration = configuration; 142 } 143 144 /** 145 * Get the configuration for this service manager. 146 * 147 * @return Broker configuration. 148 */ 149 public Configuration getConfiguration() 150 { 151 return configuration; 152 } 153 154 /** 155 * Initialize this service manager. 156 * @throws InitializationException if the initialization fails 157 */ 158 public void init() throws InitializationException 159 { 160 // Check: 161 // 162 // 1. The configuration has been set. 163 // 2. Make sure the application root has been set. 164 165 // FIXME: Make some service framework exceptions to throw in 166 // the event these requirements aren't satisfied. 167 168 // Create the mapping between service names 169 // and their classes. 170 initMapping(); 171 172 // Start services that have their 'earlyInit' 173 // property set to 'true'. 174 initServices(false); 175 } 176 177 /** 178 * Set an application specific service object 179 * that can be used by application specific 180 * services. 181 * 182 * @param name name of service object 183 * @param value value of service object 184 */ 185 public void setServiceObject(String name, Object value) 186 { 187 serviceObjects.put(name, value); 188 } 189 190 /** 191 * Get an application specific service object. 192 * 193 * @param name the name of the service object 194 * @return Object application specific service object 195 */ 196 public Object getServiceObject(String name) 197 { 198 return serviceObjects.get(name); 199 } 200 201 /** 202 * Check recursively if the given checkIfc interface is among the implemented 203 * interfaces 204 * 205 * @param checkIfc interface to check for 206 * @param interfaces interfaces to scan 207 * @return true if the interface is implemented 208 */ 209 private boolean checkForInterface(Class<?> checkIfc, Class<?>[] interfaces) 210 { 211 for (Class<?> ifc : interfaces) 212 { 213 if (ifc == checkIfc) 214 { 215 return true; 216 } 217 218 Class<?>[] subInterfaces = ifc.getInterfaces(); 219 if (checkForInterface(checkIfc, subInterfaces)) 220 { 221 return true; 222 } 223 } 224 225 return false; 226 } 227 228 /** 229 * Creates a mapping between Service names and class names. 230 * 231 * The mapping is built according to settings present in 232 * TurbineResources.properties. The entries should have the 233 * following form: 234 * 235 * <pre> 236 * services.MyService.classname=com.mycompany.MyServiceImpl 237 * services.MyOtherService.classname=com.mycompany.MyOtherServiceImpl 238 * </pre> 239 * 240 * <br> 241 * 242 * Generic ServiceBroker provides no Services. 243 * @throws InitializationException if a service class could not be found 244 */ 245 protected void initMapping() throws InitializationException 246 { 247 // we need to temporarily store the earlyInit flags to avoid 248 // ConcurrentModificationExceptions 249 Map<String, String> earlyInitFlags = new LinkedHashMap<>(); 250 251 /* 252 * These keys returned in an order that corresponds 253 * to the order the services are listed in 254 * the TR.props. 255 */ 256 for (Iterator<String> keys = configuration.getKeys(); keys.hasNext();) 257 { 258 String key = keys.next(); 259 String[] keyParts = StringUtils.split(key, "."); 260 261 if (keyParts.length == 3 262 && (keyParts[0] + ".").equals(SERVICE_PREFIX) 263 && ("." + keyParts[2]).equals(CLASSNAME_SUFFIX)) 264 { 265 String serviceKey = keyParts[1]; 266 log.info("Added Mapping for Service: {}", serviceKey); 267 268 if (!mapping.containsKey(serviceKey)) 269 { 270 String className = configuration.getString(key); 271 try 272 { 273 Class<?> clazz = Class.forName(className); 274 mapping.put(serviceKey, clazz); 275 276 // detect TurbineServiceProviders 277 if (checkForInterface(TurbineServiceProvider.class, clazz.getInterfaces())) 278 { 279 log.info("Found a TurbineServiceProvider: {} - initializing it early", serviceKey); 280 earlyInitFlags.put(SERVICE_PREFIX + serviceKey + ".earlyInit", "true"); 281 } 282 } 283 // those two errors must be passed to the VM 284 catch (ThreadDeath t) 285 { 286 throw t; 287 } 288 catch (OutOfMemoryError t) 289 { 290 throw t; 291 } 292 catch (ClassNotFoundException | NoClassDefFoundError e) 293 { 294 throw new InitializationException("Class " + className + 295 " is unavailable. Check your jars and classes.", e); 296 } 297 } 298 } 299 } 300 301 for (Map.Entry<String, String> entry : earlyInitFlags.entrySet()) 302 { 303 configuration.setProperty(entry.getKey(), entry.getValue()); 304 } 305 } 306 307 /** 308 * Determines whether a service is registered in the configured 309 * <code>TurbineResources.properties</code>. 310 * 311 * @param serviceName The name of the service whose existence to check. 312 * @return Registration predicate for the desired services. 313 */ 314 @Override 315 public boolean isRegistered(String serviceName) 316 { 317 return (services.get(serviceName) != null); 318 } 319 320 /** 321 * Returns an Iterator over all known service names. 322 * 323 * @return An Iterator of service names. 324 */ 325 public Iterator<String> getServiceNames() 326 { 327 return mapping.keySet().iterator(); 328 } 329 330 /** 331 * Returns an Iterator over all known service names beginning with 332 * the provided prefix. 333 * 334 * @param prefix The prefix against which to test. 335 * @return An Iterator of service names which match the prefix. 336 */ 337 public Iterator<String> getServiceNames(String prefix) 338 { 339 Set<String> keys = new LinkedHashSet<>(mapping.keySet()); 340 341 keys.removeIf(key -> !key.startsWith(prefix)); 342 343 return keys.iterator(); 344 } 345 346 /** 347 * Performs early initialization of specified service. 348 * 349 * @param name The name of the service (generally the 350 * <code>SERVICE_NAME</code> constant of the service's interface 351 * definition). 352 * @throws InitializationException Initialization of this 353 * service was not successful. 354 */ 355 @Override 356 public synchronized void initService(String name) 357 throws InitializationException 358 { 359 // Calling getServiceInstance(name) assures that the Service 360 // implementation has its name and broker reference set before 361 // initialization. 362 Service instance = getServiceInstance(name); 363 364 if (!instance.getInit()) 365 { 366 // this call might result in an indirect recursion 367 instance.init(); 368 } 369 } 370 371 /** 372 * Performs early initialization of all services. Failed early 373 * initialization of a Service may be non-fatal to the system, 374 * thus any exceptions are logged and the initialization process 375 * continues. 376 */ 377 public void initServices() 378 { 379 try 380 { 381 initServices(false); 382 } 383 catch (InstantiationException | InitializationException notThrown) 384 { 385 log.debug("Caught non fatal exception", notThrown); 386 } 387 } 388 389 /** 390 * Performs early initialization of all services. You can decide 391 * to handle failed initializations if you wish, but then 392 * after one service fails, the other will not have the chance 393 * to initialize. 394 * 395 * @param report <code>true</code> if you want exceptions thrown. 396 * @throws InstantiationException if the service could not be instantiated 397 * @throws InitializationException if the service could not be initialized 398 */ 399 public void initServices(boolean report) 400 throws InstantiationException, InitializationException 401 { 402 if (report) 403 { 404 // Throw exceptions 405 for (Iterator<String> names = getServiceNames(); names.hasNext();) 406 { 407 doInitService(names.next()); 408 } 409 } 410 else 411 { 412 // Eat exceptions 413 for (Iterator<String> names = getServiceNames(); names.hasNext();) 414 { 415 try 416 { 417 doInitService(names.next()); 418 } 419 // In case of an exception, file an error message; the 420 // system may be still functional, though. 421 catch (InstantiationException | InitializationException e) 422 { 423 log.error(e); 424 } 425 } 426 } 427 log.info("Finished initializing all services!"); 428 } 429 430 /** 431 * Internal utility method for use in {@link #initServices(boolean)} 432 * to prevent duplication of code. 433 */ 434 private void doInitService(String name) 435 throws InstantiationException, InitializationException 436 { 437 // Only start up services that have their earlyInit flag set. 438 if (getConfiguration(name).getBoolean("earlyInit", false)) 439 { 440 log.info("Start Initializing service (early): {}", name); 441 initService(name); 442 log.info("Finish Initializing service (early): {}", name); 443 } 444 } 445 446 /** 447 * Shuts down a <code>Service</code>, releasing resources 448 * allocated by an <code>Service</code>, and returns it to its 449 * initial (uninitialized) state. 450 * 451 * @param name The name of the <code>Service</code> to be 452 * uninitialized. 453 */ 454 @Override 455 public synchronized void shutdownService(String name) 456 { 457 try 458 { 459 Service service = getServiceInstance(name); 460 if (service != null && service.getInit()) 461 { 462 service.shutdown(); 463 464 if (service.getInit() && service instanceof BaseService) 465 { 466 // BaseService::shutdown() does this by default, 467 // but could've been overriden poorly. 468 ((BaseService) service).setInit(false); 469 } 470 } 471 } 472 catch (InstantiationException e) 473 { 474 // Assuming harmless -- log the error and continue. 475 log.error("Shutdown of a nonexistent Service '" 476 + name + "' was requested", e); 477 } 478 } 479 480 /** 481 * Shuts down all Turbine services, releasing allocated resources and 482 * returning them to their initial (uninitialized) state. 483 */ 484 @Override 485 public void shutdownServices() 486 { 487 log.info("Shutting down all services!"); 488 489 String serviceName = null; 490 491 /* 492 * Now we want to reverse the order of 493 * this list. This functionality should be added to 494 * the ExtendedProperties in the commons but 495 * this will fix the problem for now. 496 */ 497 498 ArrayList<String> reverseServicesList = new ArrayList<>(); 499 500 for (Iterator<String> serviceNames = getServiceNames(); serviceNames.hasNext();) 501 { 502 serviceName = serviceNames.next(); 503 reverseServicesList.add(0, serviceName); 504 } 505 506 for (String s : reverseServicesList) 507 { 508 serviceName = s; 509 log.info("Shutting down service: {}", serviceName); 510 shutdownService(serviceName); 511 } 512 } 513 514 /** 515 * Returns an instance of requested Service. 516 * 517 * @param name The name of the Service requested. 518 * @return An instance of requested Service. 519 * @throws InstantiationException if the service is unknown or 520 * can't be initialized. 521 */ 522 @Override 523 public Object getService(String name) throws InstantiationException 524 { 525 Service service; 526 527 if (this.isLocalService(name)) 528 { 529 try 530 { 531 service = getServiceInstance(name); 532 if (!service.getInit()) 533 { 534 synchronized (service.getClass()) 535 { 536 if (!service.getInit()) 537 { 538 log.info("Start Initializing service (late): {}", name); 539 service.init(); 540 log.info("Finish Initializing service (late): {}", name); 541 } 542 } 543 } 544 if (!service.getInit()) 545 { 546 // this exception will be caught & rethrown by this very method. 547 // getInit() returning false indicates some initialization issue, 548 // which in turn prevents the InitableBroker from passing a 549 // reference to a working instance of the initable to the client. 550 throw new InitializationException( 551 "init() failed to initialize service " + name); 552 } 553 return service; 554 } 555 catch (InitializationException e) 556 { 557 throw new InstantiationException("Service " + name + 558 " failed to initialize", e); 559 } 560 } 561 else if (this.isNonLocalService(name)) 562 { 563 return this.getNonLocalService(name); 564 } 565 else 566 { 567 throw new InstantiationException( 568 "ServiceBroker: unknown service " + name 569 + " requested"); 570 } 571 } 572 573 /** 574 * Retrieves an instance of a Service without triggering late 575 * initialization. 576 * 577 * Early initialization of a Service can require access to Service 578 * properties. The Service must have its name and serviceBroker 579 * set by then. Therefore, before calling 580 * Initable.initClass(Object), the class must be instantiated with 581 * InitableBroker.getInitableInstance(), and 582 * Service.setServiceBroker() and Service.setName() must be 583 * called. This calls for two - level accessing the Services 584 * instances. 585 * 586 * @param name The name of the service requested. 587 * 588 * @return the Service instance 589 * 590 * @throws InstantiationException The service is unknown or 591 * can't be initialized. 592 */ 593 protected Service getServiceInstance(String name) 594 throws InstantiationException 595 { 596 Service service = services.get(name); 597 598 if (service == null) 599 { 600 serviceLock.lock(); 601 602 try 603 { 604 // Double check 605 service = services.get(name); 606 607 if (service == null) 608 { 609 if (!this.isLocalService(name)) 610 { 611 throw new InstantiationException( 612 "ServiceBroker: unknown service " + name 613 + " requested"); 614 } 615 616 try 617 { 618 Class<?> clazz = mapping.get(name); 619 620 try 621 { 622 service = (Service) clazz.getDeclaredConstructor().newInstance(); 623 624 // check if the newly created service is also a 625 // service provider - if so then remember it 626 if (service instanceof TurbineServiceProvider) 627 { 628 Service _service = this.serviceProviderInstanceMap.putIfAbsent(name,service); 629 if (_service != null) 630 { 631 service = _service; 632 } 633 } 634 } 635 // those two errors must be passed to the VM 636 catch (ClassCastException e) 637 { 638 throw new InstantiationException("Class " + clazz + 639 " doesn't implement the Service interface", e); 640 } 641 catch (ThreadDeath | OutOfMemoryError t) 642 { 643 throw t; 644 } 645 catch (Throwable t) 646 { 647 throw new InstantiationException("Failed to instantiate " + clazz, t); 648 } 649 } 650 catch (InstantiationException e) 651 { 652 throw new InstantiationException( 653 "Failed to instantiate service " + name, e); 654 } 655 service.setServiceBroker(this); 656 service.setName(name); 657 Service _service = services.putIfAbsent(name, service); 658 if (_service != null) // Unlikely 659 { 660 service = _service; 661 } 662 } 663 } 664 finally 665 { 666 serviceLock.unlock(); 667 } 668 } 669 670 return service; 671 } 672 673 /** 674 * Returns the configuration for the specified service. 675 * 676 * @param name The name of the service. 677 * @return Configuration of requested Service. 678 */ 679 @Override 680 public Configuration getConfiguration(String name) 681 { 682 return configuration.subset(SERVICE_PREFIX + name); 683 } 684 685 /** 686 * Set the application root. 687 * 688 * @param applicationRoot application root 689 */ 690 public void setApplicationRoot(String applicationRoot) 691 { 692 this.applicationRoot = applicationRoot; 693 } 694 695 /** 696 * Get the application root as set by 697 * the parent application. 698 * 699 * @return String application root 700 */ 701 @Override 702 public String getApplicationRoot() 703 { 704 return applicationRoot; 705 } 706 707 /** 708 * Determines if the requested service is managed by this 709 * ServiceBroker. 710 * 711 * @param name The name of the Service requested. 712 * @return true if the service is managed by the this ServiceBroker 713 */ 714 protected boolean isLocalService(String name) 715 { 716 return this.mapping.containsKey(name); 717 } 718 719 /** 720 * Determines if the requested service is managed by an initialized 721 * TurbineServiceProvider. We use the service names to lookup 722 * the TurbineServiceProvider to ensure that we get a fully 723 * initialized service. 724 * 725 * @param name The name of the Service requested. 726 * @return true if the service is managed by a TurbineServiceProvider 727 */ 728 protected boolean isNonLocalService(String name) 729 { 730 TurbineServiceProvider turbineServiceProvider = null; 731 732 for (Map.Entry<String, Service> entry : this.serviceProviderInstanceMap.entrySet()) 733 { 734 turbineServiceProvider = (TurbineServiceProvider) this.getService(entry.getKey()); 735 736 if (turbineServiceProvider.exists(name)) 737 { 738 return true; 739 } 740 } 741 742 return false; 743 } 744 745 /** 746 * Get a non-local service managed by a TurbineServiceProvider. 747 * 748 * @param name The name of the Service requested. 749 * @return the requested service 750 * @throws InstantiationException the service couldn't be instantiated 751 */ 752 protected Object getNonLocalService(String name) 753 throws InstantiationException 754 { 755 TurbineServiceProvider turbineServiceProvider = null; 756 757 for (Map.Entry<String, Service> entry : this.serviceProviderInstanceMap.entrySet()) 758 { 759 turbineServiceProvider = (TurbineServiceProvider) this.getService(entry.getKey()); 760 761 if (turbineServiceProvider.exists(name)) 762 { 763 return turbineServiceProvider.get(name); 764 } 765 } 766 767 throw new InstantiationException( 768 "ServiceBroker: unknown non-local service " + name 769 + " requested"); 770 } 771}