001package org.apache.turbine.services;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022
023import java.util.ArrayList;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.LinkedHashSet;
027import java.util.Map;
028import java.util.Set;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.locks.ReentrantLock;
031
032import org.apache.commons.configuration2.Configuration;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.Logger;
036
037/**
038 * A generic implementation of a <code>ServiceBroker</code> which
039 * provides:
040 *
041 * <ul>
042 * <li>Maintaining service name to class name mapping, allowing
043 * pluggable service implementations.</li>
044 * <li>Providing <code>Services</code> with a configuration based on
045 * system wide configuration mechanism.</li>
046 * <li>Integration of TurbineServiceProviders for looking up
047 * non-local services</li>
048 * </ul>
049 *
050 * @author <a href="mailto:burton@apache.org">Kevin Burton</a>
051 * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
052 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
053 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
054 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
055 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
056 * @version $Id$
057 */
058public abstract class BaseServiceBroker implements ServiceBroker
059{
060    /**
061     * Mapping of Service names to class names, keep order.
062     */
063    private final Map<String, Class<?>> mapping = new LinkedHashMap<>();
064
065    /**
066     * A repository of Service instances.
067     */
068    private final ConcurrentHashMap<String, Service> services = new ConcurrentHashMap<>();
069
070    /**
071     * Lock access during service initialization
072     */
073    private final ReentrantLock serviceLock = new ReentrantLock();
074
075    /**
076     * Configuration for the services broker.
077     * The configuration should be set by the application
078     * in which the services framework is running.
079     */
080    private Configuration configuration;
081
082    /**
083     * A prefix for <code>Service</code> properties in
084     * TurbineResource.properties.
085     */
086    public static final String SERVICE_PREFIX = "services.";
087
088    /**
089     * A <code>Service</code> property determining its implementing
090     * class name .
091     */
092    public static final String CLASSNAME_SUFFIX = ".classname";
093
094    /**
095     * These are objects that the parent application
096     * can provide so that application specific
097     * services have a mechanism to retrieve specialized
098     * information. For example, in Turbine there are services
099     * that require the RunData object: these services can
100     * retrieve the RunData object that Turbine has placed
101     * in the service manager. This alleviates us of
102     * the requirement of having init(Object) all
103     * together.
104     */
105    private final ConcurrentHashMap<String, Object> serviceObjects = new ConcurrentHashMap<>();
106
107    /** Logging */
108    private static final Logger log = LogManager.getLogger(BaseServiceBroker.class);
109
110    /**
111     * Application root path as set by the
112     * parent application.
113     */
114    private String applicationRoot;
115
116    /**
117     * mapping from service names to instances of TurbineServiceProviders
118     */
119    private final ConcurrentHashMap<String, Service> serviceProviderInstanceMap = new ConcurrentHashMap<>();
120
121    /**
122     * Default constructor, protected as to only be usable by subclasses.
123     *
124     * This constructor does nothing.
125     */
126    protected BaseServiceBroker()
127    {
128        // nothing to do
129    }
130
131    /**
132     * Set the configuration object for the services broker.
133     * This is the configuration that contains information
134     * about all services in the care of this service
135     * manager.
136     *
137     * @param configuration Broker configuration.
138     */
139    public void setConfiguration(Configuration configuration)
140    {
141        this.configuration = configuration;
142    }
143
144    /**
145     * Get the configuration for this service manager.
146     *
147     * @return Broker configuration.
148     */
149    public Configuration getConfiguration()
150    {
151        return configuration;
152    }
153
154    /**
155     * Initialize this service manager.
156     * @throws InitializationException if the initialization fails
157     */
158    public void init() throws InitializationException
159    {
160        // Check:
161        //
162        // 1. The configuration has been set.
163        // 2. Make sure the application root has been set.
164
165        // FIXME: Make some service framework exceptions to throw in
166        // the event these requirements aren't satisfied.
167
168        // Create the mapping between service names
169        // and their classes.
170        initMapping();
171
172        // Start services that have their 'earlyInit'
173        // property set to 'true'.
174        initServices(false);
175    }
176
177    /**
178     * Set an application specific service object
179     * that can be used by application specific
180     * services.
181     *
182     * @param name name of service object
183     * @param value value of service object
184     */
185    public void setServiceObject(String name, Object value)
186    {
187        serviceObjects.put(name, value);
188    }
189
190    /**
191     * Get an application specific service object.
192     *
193     * @param name the name of the service object
194     * @return Object application specific service object
195     */
196    public Object getServiceObject(String name)
197    {
198        return serviceObjects.get(name);
199    }
200
201    /**
202     * Check recursively if the given checkIfc interface is among the implemented
203     * interfaces
204     *
205     * @param checkIfc interface to check for
206     * @param interfaces interfaces to scan
207     * @return true if the interface is implemented
208     */
209    private boolean checkForInterface(Class<?> checkIfc, Class<?>[] interfaces)
210    {
211        for (Class<?> ifc : interfaces)
212        {
213            if (ifc == checkIfc)
214            {
215                return true;
216            }
217
218            Class<?>[] subInterfaces = ifc.getInterfaces();
219            if (checkForInterface(checkIfc, subInterfaces))
220            {
221                return true;
222            }
223        }
224
225        return false;
226    }
227
228    /**
229     * Creates a mapping between Service names and class names.
230     *
231     * The mapping is built according to settings present in
232     * TurbineResources.properties.  The entries should have the
233     * following form:
234     *
235     * <pre>
236     * services.MyService.classname=com.mycompany.MyServiceImpl
237     * services.MyOtherService.classname=com.mycompany.MyOtherServiceImpl
238     * </pre>
239     *
240     * <br>
241     *
242     * Generic ServiceBroker provides no Services.
243     * @throws InitializationException if a service class could not be found
244     */
245    protected void initMapping() throws InitializationException
246    {
247        // we need to temporarily store the earlyInit flags to avoid
248        // ConcurrentModificationExceptions
249        Map<String, String> earlyInitFlags = new LinkedHashMap<>();
250
251        /*
252         * These keys returned in an order that corresponds
253         * to the order the services are listed in
254         * the TR.props.
255         */
256        for (Iterator<String> keys = configuration.getKeys(); keys.hasNext();)
257        {
258            String key = keys.next();
259            String[] keyParts = StringUtils.split(key, ".");
260
261            if (keyParts.length == 3
262                    && (keyParts[0] + ".").equals(SERVICE_PREFIX)
263                    && ("." + keyParts[2]).equals(CLASSNAME_SUFFIX))
264            {
265                String serviceKey = keyParts[1];
266                log.info("Added Mapping for Service: {}", serviceKey);
267
268                if (!mapping.containsKey(serviceKey))
269                {
270                    String className = configuration.getString(key);
271                    try
272                    {
273                        Class<?> clazz = Class.forName(className);
274                        mapping.put(serviceKey, clazz);
275
276                        // detect TurbineServiceProviders
277                        if (checkForInterface(TurbineServiceProvider.class, clazz.getInterfaces()))
278                        {
279                            log.info("Found a TurbineServiceProvider: {} - initializing it early", serviceKey);
280                            earlyInitFlags.put(SERVICE_PREFIX + serviceKey + ".earlyInit", "true");
281                        }
282                    }
283                    // those two errors must be passed to the VM
284                    catch (ThreadDeath t)
285                    {
286                        throw t;
287                    }
288                    catch (OutOfMemoryError t)
289                    {
290                        throw t;
291                    }
292                    catch (ClassNotFoundException | NoClassDefFoundError e)
293                    {
294                        throw new InitializationException("Class " + className +
295                            " is unavailable. Check your jars and classes.", e);
296                    }
297                }
298            }
299        }
300
301        for (Map.Entry<String, String> entry : earlyInitFlags.entrySet())
302        {
303            configuration.setProperty(entry.getKey(), entry.getValue());
304        }
305    }
306
307    /**
308     * Determines whether a service is registered in the configured
309     * <code>TurbineResources.properties</code>.
310     *
311     * @param serviceName The name of the service whose existence to check.
312     * @return Registration predicate for the desired services.
313     */
314    @Override
315    public boolean isRegistered(String serviceName)
316    {
317        return (services.get(serviceName) != null);
318    }
319
320    /**
321     * Returns an Iterator over all known service names.
322     *
323     * @return An Iterator of service names.
324     */
325    public Iterator<String> getServiceNames()
326    {
327        return mapping.keySet().iterator();
328    }
329
330    /**
331     * Returns an Iterator over all known service names beginning with
332     * the provided prefix.
333     *
334     * @param prefix The prefix against which to test.
335     * @return An Iterator of service names which match the prefix.
336     */
337    public Iterator<String> getServiceNames(String prefix)
338    {
339        Set<String> keys = new LinkedHashSet<>(mapping.keySet());
340        for(Iterator<String> key = keys.iterator(); key.hasNext();)
341        {
342            if (!key.next().startsWith(prefix))
343            {
344                key.remove();
345            }
346        }
347
348        return keys.iterator();
349    }
350
351    /**
352     * Performs early initialization of specified service.
353     *
354     * @param name The name of the service (generally the
355     * <code>SERVICE_NAME</code> constant of the service's interface
356     * definition).
357     * @throws InitializationException Initialization of this
358     * service was not successful.
359     */
360    @Override
361    public synchronized void initService(String name)
362            throws InitializationException
363    {
364        // Calling getServiceInstance(name) assures that the Service
365        // implementation has its name and broker reference set before
366        // initialization.
367        Service instance = getServiceInstance(name);
368
369        if (!instance.getInit())
370        {
371            // this call might result in an indirect recursion
372            instance.init();
373        }
374    }
375
376    /**
377     * Performs early initialization of all services.  Failed early
378     * initialization of a Service may be non-fatal to the system,
379     * thus any exceptions are logged and the initialization process
380     * continues.
381     */
382    public void initServices()
383    {
384        try
385        {
386            initServices(false);
387        }
388        catch (InstantiationException | InitializationException notThrown)
389        {
390            log.debug("Caught non fatal exception", notThrown);
391        }
392    }
393
394    /**
395     * Performs early initialization of all services. You can decide
396     * to handle failed initializations if you wish, but then
397     * after one service fails, the other will not have the chance
398     * to initialize.
399     *
400     * @param report <code>true</code> if you want exceptions thrown.
401     * @throws InstantiationException if the service could not be instantiated
402     * @throws InitializationException if the service could not be initialized
403     */
404    public void initServices(boolean report)
405            throws InstantiationException, InitializationException
406    {
407        if (report)
408        {
409            // Throw exceptions
410            for (Iterator<String> names = getServiceNames(); names.hasNext();)
411            {
412                doInitService(names.next());
413            }
414        }
415        else
416        {
417            // Eat exceptions
418            for (Iterator<String> names = getServiceNames(); names.hasNext();)
419            {
420                try
421                {
422                    doInitService(names.next());
423                }
424                        // In case of an exception, file an error message; the
425                        // system may be still functional, though.
426                catch (InstantiationException | InitializationException e)
427                {
428                    log.error(e);
429                }
430            }
431        }
432        log.info("Finished initializing all services!");
433    }
434
435    /**
436     * Internal utility method for use in {@link #initServices(boolean)}
437     * to prevent duplication of code.
438     */
439    private void doInitService(String name)
440            throws InstantiationException, InitializationException
441    {
442        // Only start up services that have their earlyInit flag set.
443        if (getConfiguration(name).getBoolean("earlyInit", false))
444        {
445            log.info("Start Initializing service (early): {}", name);
446            initService(name);
447            log.info("Finish Initializing service (early): {}", name);
448        }
449    }
450
451    /**
452     * Shuts down a <code>Service</code>, releasing resources
453     * allocated by an <code>Service</code>, and returns it to its
454     * initial (uninitialized) state.
455     *
456     * @param name The name of the <code>Service</code> to be
457     * uninitialized.
458     */
459    @Override
460    public synchronized void shutdownService(String name)
461    {
462        try
463        {
464            Service service = getServiceInstance(name);
465            if (service != null && service.getInit())
466            {
467                service.shutdown();
468
469                if (service.getInit() && service instanceof BaseService)
470                {
471                    // BaseService::shutdown() does this by default,
472                    // but could've been overriden poorly.
473                    ((BaseService) service).setInit(false);
474                }
475            }
476        }
477        catch (InstantiationException e)
478        {
479            // Assuming harmless -- log the error and continue.
480            log.error("Shutdown of a nonexistent Service '"
481                    + name + "' was requested", e);
482        }
483    }
484
485    /**
486     * Shuts down all Turbine services, releasing allocated resources and
487     * returning them to their initial (uninitialized) state.
488     */
489    @Override
490    public void shutdownServices()
491    {
492        log.info("Shutting down all services!");
493
494        String serviceName = null;
495
496        /*
497         * Now we want to reverse the order of
498         * this list. This functionality should be added to
499         * the ExtendedProperties in the commons but
500         * this will fix the problem for now.
501         */
502
503        ArrayList<String> reverseServicesList = new ArrayList<>();
504
505        for (Iterator<String> serviceNames = getServiceNames(); serviceNames.hasNext();)
506        {
507            serviceName = serviceNames.next();
508            reverseServicesList.add(0, serviceName);
509        }
510
511        for (Iterator<String> serviceNames = reverseServicesList.iterator(); serviceNames.hasNext();)
512        {
513            serviceName = serviceNames.next();
514            log.info("Shutting down service: {}", serviceName);
515            shutdownService(serviceName);
516        }
517    }
518
519    /**
520     * Returns an instance of requested Service.
521     *
522     * @param name The name of the Service requested.
523     * @return An instance of requested Service.
524     * @throws InstantiationException if the service is unknown or
525     * can't be initialized.
526     */
527    @Override
528    public Object getService(String name) throws InstantiationException
529    {
530        Service service;
531
532        if (this.isLocalService(name))
533        {
534                try
535                {
536                    service = getServiceInstance(name);
537                    if (!service.getInit())
538                    {
539                        synchronized (service.getClass())
540                        {
541                            if (!service.getInit())
542                            {
543                                log.info("Start Initializing service (late): {}", name);
544                                service.init();
545                                log.info("Finish Initializing service (late): {}", name);
546                            }
547                        }
548                    }
549                    if (!service.getInit())
550                    {
551                        // this exception will be caught & rethrown by this very method.
552                        // getInit() returning false indicates some initialization issue,
553                        // which in turn prevents the InitableBroker from passing a
554                        // reference to a working instance of the initable to the client.
555                        throw new InitializationException(
556                                "init() failed to initialize service " + name);
557                    }
558                    return service;
559                }
560                catch (InitializationException e)
561                {
562                    throw new InstantiationException("Service " + name +
563                            " failed to initialize", e);
564                }
565        }
566        else if (this.isNonLocalService(name))
567        {
568            return this.getNonLocalService(name);
569        }
570        else
571        {
572            throw new InstantiationException(
573                "ServiceBroker: unknown service " + name
574                + " requested");
575        }
576    }
577
578    /**
579     * Retrieves an instance of a Service without triggering late
580     * initialization.
581     *
582     * Early initialization of a Service can require access to Service
583     * properties.  The Service must have its name and serviceBroker
584     * set by then.  Therefore, before calling
585     * Initable.initClass(Object), the class must be instantiated with
586     * InitableBroker.getInitableInstance(), and
587     * Service.setServiceBroker() and Service.setName() must be
588     * called.  This calls for two - level accessing the Services
589     * instances.
590     *
591     * @param name The name of the service requested.
592     *
593     * @return the Service instance
594     *
595     * @throws InstantiationException The service is unknown or
596     * can't be initialized.
597     */
598    protected Service getServiceInstance(String name)
599            throws InstantiationException
600    {
601        Service service = services.get(name);
602
603        if (service == null)
604        {
605            serviceLock.lock();
606
607            try
608            {
609                // Double check
610                service = services.get(name);
611
612                if (service == null)
613                {
614                    if (!this.isLocalService(name))
615                    {
616                        throw new InstantiationException(
617                                "ServiceBroker: unknown service " + name
618                                + " requested");
619                    }
620
621                    try
622                    {
623                        Class<?> clazz = mapping.get(name);
624
625                        try
626                        {
627                            service = (Service) clazz.newInstance();
628
629                            // check if the newly created service is also a
630                            // service provider - if so then remember it
631                            if (service instanceof TurbineServiceProvider)
632                            {
633                                Service _service = this.serviceProviderInstanceMap.putIfAbsent(name,service);
634                                if (_service != null)
635                                {
636                                    service = _service;
637                                }
638                            }
639                        }
640                        // those two errors must be passed to the VM
641                        catch (ClassCastException e)
642                        {
643                            throw new InstantiationException("Class " + clazz +
644                                    " doesn't implement the Service interface", e);
645                        }
646                        catch (ThreadDeath t)
647                        {
648                            throw t;
649                        }
650                        catch (OutOfMemoryError t)
651                        {
652                            throw t;
653                        }
654                        catch (Throwable t)
655                        {
656                            throw new InstantiationException("Failed to instantiate " + clazz, t);
657                        }
658                    }
659                    catch (InstantiationException e)
660                    {
661                        throw new InstantiationException(
662                                "Failed to instantiate service " + name, e);
663                    }
664                    service.setServiceBroker(this);
665                    service.setName(name);
666                    Service _service = services.putIfAbsent(name, service);
667                    if (_service != null) // Unlikely
668                    {
669                        service = _service;
670                    }
671                }
672            }
673            finally
674            {
675                serviceLock.unlock();
676            }
677        }
678
679        return service;
680    }
681
682    /**
683     * Returns the configuration for the specified service.
684     *
685     * @param name The name of the service.
686     * @return Configuration of requested Service.
687     */
688    @Override
689    public Configuration getConfiguration(String name)
690    {
691        return configuration.subset(SERVICE_PREFIX + name);
692    }
693
694    /**
695     * Set the application root.
696     *
697     * @param applicationRoot application root
698     */
699    public void setApplicationRoot(String applicationRoot)
700    {
701        this.applicationRoot = applicationRoot;
702    }
703
704    /**
705     * Get the application root as set by
706     * the parent application.
707     *
708     * @return String application root
709     */
710    @Override
711    public String getApplicationRoot()
712    {
713        return applicationRoot;
714    }
715
716    /**
717     * Determines if the requested service is managed by this
718     * ServiceBroker.
719     *
720     * @param name The name of the Service requested.
721     * @return true if the service is managed by the this ServiceBroker
722     */
723    protected boolean isLocalService(String name)
724    {
725        return this.mapping.containsKey(name);
726    }
727
728    /**
729     * Determines if the requested service is managed by an initialized
730     * TurbineServiceProvider. We use the service names to lookup
731     * the TurbineServiceProvider to ensure that we get a fully
732     * initialized service.
733     *
734     * @param name The name of the Service requested.
735     * @return true if the service is managed by a TurbineServiceProvider
736     */
737    protected boolean isNonLocalService(String name)
738    {
739        TurbineServiceProvider turbineServiceProvider = null;
740
741        for (Map.Entry<String, Service> entry : this.serviceProviderInstanceMap.entrySet())
742        {
743            turbineServiceProvider = (TurbineServiceProvider) this.getService(entry.getKey());
744
745            if (turbineServiceProvider.exists(name))
746            {
747                return true;
748            }
749        }
750
751        return false;
752    }
753
754    /**
755     * Get a non-local service managed by a TurbineServiceProvider.
756     *
757     * @param name The name of the Service requested.
758     * @return the requested service
759     * @throws InstantiationException the service couldn't be instantiated
760     */
761    protected Object getNonLocalService(String name)
762        throws InstantiationException
763    {
764        TurbineServiceProvider turbineServiceProvider = null;
765
766        for (Map.Entry<String, Service> entry : this.serviceProviderInstanceMap.entrySet())
767        {
768            turbineServiceProvider = (TurbineServiceProvider) this.getService(entry.getKey());
769
770            if (turbineServiceProvider.exists(name))
771            {
772                return turbineServiceProvider.get(name);
773            }
774        }
775
776        throw new InstantiationException(
777            "ServiceBroker: unknown non-local service " + name
778            + " requested");
779    }
780}