View Javadoc
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 }