1 package org.apache.turbine.services;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22
23 import java.util.ArrayList;
24 import java.util.Iterator;
25 import java.util.LinkedHashMap;
26 import java.util.LinkedHashSet;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.locks.ReentrantLock;
31
32 import org.apache.commons.configuration2.Configuration;
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.logging.log4j.LogManager;
35 import org.apache.logging.log4j.Logger;
36
37 /**
38 * A generic implementation of a <code>ServiceBroker</code> which
39 * provides:
40 *
41 * <ul>
42 * <li>Maintaining service name to class name mapping, allowing
43 * pluggable service implementations.</li>
44 * <li>Providing <code>Services</code> with a configuration based on
45 * system wide configuration mechanism.</li>
46 * <li>Integration of TurbineServiceProviders for looking up
47 * non-local services</li>
48 * </ul>
49 *
50 * @author <a href="mailto:burton@apache.org">Kevin Burton</a>
51 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
52 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
53 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
54 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
55 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
56 * @version $Id: BaseServiceBroker.java 1854787 2019-03-04 18:30:25Z tv $
57 */
58 public abstract class BaseServiceBroker implements ServiceBroker
59 {
60 /**
61 * Mapping of Service names to class names, keep order.
62 */
63 private final Map<String, Class<?>> mapping = new LinkedHashMap<String, Class<?>>();
64
65 /**
66 * A repository of Service instances.
67 */
68 private final ConcurrentHashMap<String, Service> services = new ConcurrentHashMap<String, Service>();
69
70 /**
71 * Lock access during service initialization
72 */
73 private final ReentrantLock serviceLock = new ReentrantLock();
74
75 /**
76 * Configuration for the services broker.
77 * The configuration should be set by the application
78 * in which the services framework is running.
79 */
80 private Configuration configuration;
81
82 /**
83 * A prefix for <code>Service</code> properties in
84 * TurbineResource.properties.
85 */
86 public static final String SERVICE_PREFIX = "services.";
87
88 /**
89 * A <code>Service</code> property determining its implementing
90 * class name .
91 */
92 public static final String CLASSNAME_SUFFIX = ".classname";
93
94 /**
95 * These are objects that the parent application
96 * can provide so that application specific
97 * services have a mechanism to retrieve specialized
98 * information. For example, in Turbine there are services
99 * 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<String, Object>();
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<String, Service>();
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<String, String>();
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<String>(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<String>();
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 }