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 for(Iterator<String> key = keys.iterator(); key.hasNext();) 341 { 342 if (!key.next().startsWith(prefix)) 343 { 344 key.remove(); 345 } 346 } 347 348 return keys.iterator(); 349 } 350 351 /** 352 * Performs early initialization of specified service. 353 * 354 * @param name The name of the service (generally the 355 * <code>SERVICE_NAME</code> constant of the service's interface 356 * definition). 357 * @throws InitializationException Initialization of this 358 * service was not successful. 359 */ 360 @Override 361 public synchronized void initService(String name) 362 throws InitializationException 363 { 364 // Calling getServiceInstance(name) assures that the Service 365 // implementation has its name and broker reference set before 366 // initialization. 367 Service instance = getServiceInstance(name); 368 369 if (!instance.getInit()) 370 { 371 // this call might result in an indirect recursion 372 instance.init(); 373 } 374 } 375 376 /** 377 * Performs early initialization of all services. Failed early 378 * initialization of a Service may be non-fatal to the system, 379 * thus any exceptions are logged and the initialization process 380 * continues. 381 */ 382 public void initServices() 383 { 384 try 385 { 386 initServices(false); 387 } 388 catch (InstantiationException | InitializationException notThrown) 389 { 390 log.debug("Caught non fatal exception", notThrown); 391 } 392 } 393 394 /** 395 * Performs early initialization of all services. You can decide 396 * to handle failed initializations if you wish, but then 397 * after one service fails, the other will not have the chance 398 * to initialize. 399 * 400 * @param report <code>true</code> if you want exceptions thrown. 401 * @throws InstantiationException if the service could not be instantiated 402 * @throws InitializationException if the service could not be initialized 403 */ 404 public void initServices(boolean report) 405 throws InstantiationException, InitializationException 406 { 407 if (report) 408 { 409 // Throw exceptions 410 for (Iterator<String> names = getServiceNames(); names.hasNext();) 411 { 412 doInitService(names.next()); 413 } 414 } 415 else 416 { 417 // Eat exceptions 418 for (Iterator<String> names = getServiceNames(); names.hasNext();) 419 { 420 try 421 { 422 doInitService(names.next()); 423 } 424 // In case of an exception, file an error message; the 425 // system may be still functional, though. 426 catch (InstantiationException | InitializationException e) 427 { 428 log.error(e); 429 } 430 } 431 } 432 log.info("Finished initializing all services!"); 433 } 434 435 /** 436 * Internal utility method for use in {@link #initServices(boolean)} 437 * to prevent duplication of code. 438 */ 439 private void doInitService(String name) 440 throws InstantiationException, InitializationException 441 { 442 // Only start up services that have their earlyInit flag set. 443 if (getConfiguration(name).getBoolean("earlyInit", false)) 444 { 445 log.info("Start Initializing service (early): {}", name); 446 initService(name); 447 log.info("Finish Initializing service (early): {}", name); 448 } 449 } 450 451 /** 452 * Shuts down a <code>Service</code>, releasing resources 453 * allocated by an <code>Service</code>, and returns it to its 454 * initial (uninitialized) state. 455 * 456 * @param name The name of the <code>Service</code> to be 457 * uninitialized. 458 */ 459 @Override 460 public synchronized void shutdownService(String name) 461 { 462 try 463 { 464 Service service = getServiceInstance(name); 465 if (service != null && service.getInit()) 466 { 467 service.shutdown(); 468 469 if (service.getInit() && service instanceof BaseService) 470 { 471 // BaseService::shutdown() does this by default, 472 // but could've been overriden poorly. 473 ((BaseService) service).setInit(false); 474 } 475 } 476 } 477 catch (InstantiationException e) 478 { 479 // Assuming harmless -- log the error and continue. 480 log.error("Shutdown of a nonexistent Service '" 481 + name + "' was requested", e); 482 } 483 } 484 485 /** 486 * Shuts down all Turbine services, releasing allocated resources and 487 * returning them to their initial (uninitialized) state. 488 */ 489 @Override 490 public void shutdownServices() 491 { 492 log.info("Shutting down all services!"); 493 494 String serviceName = null; 495 496 /* 497 * Now we want to reverse the order of 498 * this list. This functionality should be added to 499 * the ExtendedProperties in the commons but 500 * this will fix the problem for now. 501 */ 502 503 ArrayList<String> reverseServicesList = new ArrayList<>(); 504 505 for (Iterator<String> serviceNames = getServiceNames(); serviceNames.hasNext();) 506 { 507 serviceName = serviceNames.next(); 508 reverseServicesList.add(0, serviceName); 509 } 510 511 for (Iterator<String> serviceNames = reverseServicesList.iterator(); serviceNames.hasNext();) 512 { 513 serviceName = serviceNames.next(); 514 log.info("Shutting down service: {}", serviceName); 515 shutdownService(serviceName); 516 } 517 } 518 519 /** 520 * Returns an instance of requested Service. 521 * 522 * @param name The name of the Service requested. 523 * @return An instance of requested Service. 524 * @throws InstantiationException if the service is unknown or 525 * can't be initialized. 526 */ 527 @Override 528 public Object getService(String name) throws InstantiationException 529 { 530 Service service; 531 532 if (this.isLocalService(name)) 533 { 534 try 535 { 536 service = getServiceInstance(name); 537 if (!service.getInit()) 538 { 539 synchronized (service.getClass()) 540 { 541 if (!service.getInit()) 542 { 543 log.info("Start Initializing service (late): {}", name); 544 service.init(); 545 log.info("Finish Initializing service (late): {}", name); 546 } 547 } 548 } 549 if (!service.getInit()) 550 { 551 // this exception will be caught & rethrown by this very method. 552 // getInit() returning false indicates some initialization issue, 553 // which in turn prevents the InitableBroker from passing a 554 // reference to a working instance of the initable to the client. 555 throw new InitializationException( 556 "init() failed to initialize service " + name); 557 } 558 return service; 559 } 560 catch (InitializationException e) 561 { 562 throw new InstantiationException("Service " + name + 563 " failed to initialize", e); 564 } 565 } 566 else if (this.isNonLocalService(name)) 567 { 568 return this.getNonLocalService(name); 569 } 570 else 571 { 572 throw new InstantiationException( 573 "ServiceBroker: unknown service " + name 574 + " requested"); 575 } 576 } 577 578 /** 579 * Retrieves an instance of a Service without triggering late 580 * initialization. 581 * 582 * Early initialization of a Service can require access to Service 583 * properties. The Service must have its name and serviceBroker 584 * set by then. Therefore, before calling 585 * Initable.initClass(Object), the class must be instantiated with 586 * InitableBroker.getInitableInstance(), and 587 * Service.setServiceBroker() and Service.setName() must be 588 * called. This calls for two - level accessing the Services 589 * instances. 590 * 591 * @param name The name of the service requested. 592 * 593 * @return the Service instance 594 * 595 * @throws InstantiationException The service is unknown or 596 * can't be initialized. 597 */ 598 protected Service getServiceInstance(String name) 599 throws InstantiationException 600 { 601 Service service = services.get(name); 602 603 if (service == null) 604 { 605 serviceLock.lock(); 606 607 try 608 { 609 // Double check 610 service = services.get(name); 611 612 if (service == null) 613 { 614 if (!this.isLocalService(name)) 615 { 616 throw new InstantiationException( 617 "ServiceBroker: unknown service " + name 618 + " requested"); 619 } 620 621 try 622 { 623 Class<?> clazz = mapping.get(name); 624 625 try 626 { 627 service = (Service) clazz.newInstance(); 628 629 // check if the newly created service is also a 630 // service provider - if so then remember it 631 if (service instanceof TurbineServiceProvider) 632 { 633 Service _service = this.serviceProviderInstanceMap.putIfAbsent(name,service); 634 if (_service != null) 635 { 636 service = _service; 637 } 638 } 639 } 640 // those two errors must be passed to the VM 641 catch (ClassCastException e) 642 { 643 throw new InstantiationException("Class " + clazz + 644 " doesn't implement the Service interface", e); 645 } 646 catch (ThreadDeath t) 647 { 648 throw t; 649 } 650 catch (OutOfMemoryError t) 651 { 652 throw t; 653 } 654 catch (Throwable t) 655 { 656 throw new InstantiationException("Failed to instantiate " + clazz, t); 657 } 658 } 659 catch (InstantiationException e) 660 { 661 throw new InstantiationException( 662 "Failed to instantiate service " + name, e); 663 } 664 service.setServiceBroker(this); 665 service.setName(name); 666 Service _service = services.putIfAbsent(name, service); 667 if (_service != null) // Unlikely 668 { 669 service = _service; 670 } 671 } 672 } 673 finally 674 { 675 serviceLock.unlock(); 676 } 677 } 678 679 return service; 680 } 681 682 /** 683 * Returns the configuration for the specified service. 684 * 685 * @param name The name of the service. 686 * @return Configuration of requested Service. 687 */ 688 @Override 689 public Configuration getConfiguration(String name) 690 { 691 return configuration.subset(SERVICE_PREFIX + name); 692 } 693 694 /** 695 * Set the application root. 696 * 697 * @param applicationRoot application root 698 */ 699 public void setApplicationRoot(String applicationRoot) 700 { 701 this.applicationRoot = applicationRoot; 702 } 703 704 /** 705 * Get the application root as set by 706 * the parent application. 707 * 708 * @return String application root 709 */ 710 @Override 711 public String getApplicationRoot() 712 { 713 return applicationRoot; 714 } 715 716 /** 717 * Determines if the requested service is managed by this 718 * ServiceBroker. 719 * 720 * @param name The name of the Service requested. 721 * @return true if the service is managed by the this ServiceBroker 722 */ 723 protected boolean isLocalService(String name) 724 { 725 return this.mapping.containsKey(name); 726 } 727 728 /** 729 * Determines if the requested service is managed by an initialized 730 * TurbineServiceProvider. We use the service names to lookup 731 * the TurbineServiceProvider to ensure that we get a fully 732 * initialized service. 733 * 734 * @param name The name of the Service requested. 735 * @return true if the service is managed by a TurbineServiceProvider 736 */ 737 protected boolean isNonLocalService(String name) 738 { 739 TurbineServiceProvider turbineServiceProvider = null; 740 741 for (Map.Entry<String, Service> entry : this.serviceProviderInstanceMap.entrySet()) 742 { 743 turbineServiceProvider = (TurbineServiceProvider) this.getService(entry.getKey()); 744 745 if (turbineServiceProvider.exists(name)) 746 { 747 return true; 748 } 749 } 750 751 return false; 752 } 753 754 /** 755 * Get a non-local service managed by a TurbineServiceProvider. 756 * 757 * @param name The name of the Service requested. 758 * @return the requested service 759 * @throws InstantiationException the service couldn't be instantiated 760 */ 761 protected Object getNonLocalService(String name) 762 throws InstantiationException 763 { 764 TurbineServiceProvider turbineServiceProvider = null; 765 766 for (Map.Entry<String, Service> entry : this.serviceProviderInstanceMap.entrySet()) 767 { 768 turbineServiceProvider = (TurbineServiceProvider) this.getService(entry.getKey()); 769 770 if (turbineServiceProvider.exists(name)) 771 { 772 return turbineServiceProvider.get(name); 773 } 774 } 775 776 throw new InstantiationException( 777 "ServiceBroker: unknown non-local service " + name 778 + " requested"); 779 } 780}