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$
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<>();
64  
65      /**
66       * A repository of Service instances.
67       */
68      private final ConcurrentHashMap<String, Service> services = new ConcurrentHashMap<>();
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<>();
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 }