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