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