| package org.apache.fulcrum.yaafi.framework.container; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import java.io.File; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.configuration.DefaultConfiguration; |
| import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; |
| import org.apache.avalon.framework.container.ContainerUtil; |
| import org.apache.avalon.framework.context.Context; |
| import org.apache.avalon.framework.context.ContextException; |
| import org.apache.avalon.framework.context.DefaultContext; |
| import org.apache.avalon.framework.logger.Logger; |
| import org.apache.avalon.framework.parameters.ParameterException; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.fulcrum.yaafi.framework.component.AvalonServiceComponentImpl; |
| import org.apache.fulcrum.yaafi.framework.component.ServiceComponent; |
| import org.apache.fulcrum.yaafi.framework.configuration.ComponentConfigurationPropertiesResolver; |
| import org.apache.fulcrum.yaafi.framework.configuration.ComponentConfigurationPropertiesResolverImpl; |
| import org.apache.fulcrum.yaafi.framework.constant.AvalonYaafiConstants; |
| import org.apache.fulcrum.yaafi.framework.context.AvalonToYaafiContextMapper; |
| import org.apache.fulcrum.yaafi.framework.context.YaafiToAvalonContextMapper; |
| import org.apache.fulcrum.yaafi.framework.crypto.CryptoStreamFactory; |
| import org.apache.fulcrum.yaafi.framework.role.RoleConfigurationParser; |
| import org.apache.fulcrum.yaafi.framework.role.RoleConfigurationParserImpl; |
| import org.apache.fulcrum.yaafi.framework.role.RoleEntry; |
| import org.apache.fulcrum.yaafi.framework.util.ConfigurationUtil; |
| import org.apache.fulcrum.yaafi.framework.util.InputStreamLocator; |
| import org.apache.fulcrum.yaafi.framework.util.ToStringBuilder; |
| import org.apache.fulcrum.yaafi.framework.util.Validate; |
| |
| /** |
| * Yet another avalon framework implementation (YAAFI). |
| * |
| * @author <a href="mailto:siegfried.goeschl@it20one.at">Siegfried Goeschl</a> |
| */ |
| |
| public class ServiceContainerImpl implements ServiceContainer, ServiceConstants { |
| /** Default timeout before disposing the container */ |
| private static final int DISPOSAL_DELAY_DEFAULT = 0; |
| |
| /** Default timeout before reconfiguring the container or services */ |
| private static final int RECONFIGURATION_DELAY_DEFAULT = 2000; |
| |
| /** The role configuration file to be used */ |
| private String componentRolesLocation; |
| |
| /** is the component role file encrypted? */ |
| private String isComponentRolesEncrypted; |
| |
| /** which flavour of component role file we have to parse? */ |
| private String componentRolesFlavour; |
| |
| /** The service configuration file to be used */ |
| private String componentConfigurationLocation; |
| |
| /** is the component configuration file encrypted? */ |
| private String isComponentConfigurationEncrypted; |
| |
| /** The parameters file to be used */ |
| private String parametersLocation; |
| |
| /** is the parameters file encrypted? */ |
| private String isParametersEncrypted; |
| |
| /** The application directory aka the current working directory */ |
| private File applicationRootDir; |
| |
| /** The directory for storing temporary files */ |
| private File tempRootDir; |
| |
| /** The logger to be used passed by the caller */ |
| private Logger logger; |
| |
| /** The service manager passed to the container */ |
| private ServiceManager parentServiceManager; |
| |
| /** The list of services instantiated */ |
| private List<ServiceComponent> serviceList; |
| |
| /** The map of services used for the lookup */ |
| private Map<String, ServiceComponent> serviceMap; |
| |
| /** The Avalon role configuration loaded by this class */ |
| private Configuration roleConfiguration; |
| |
| /** The Avalon service configuration loaded by this class */ |
| private Configuration serviceConfiguration; |
| |
| /** The temporary Avalon context passed to the implementation */ |
| private Context callerContext; |
| |
| /** The default Avalon context passed to the services */ |
| private Context context; |
| |
| /** The default Avalon parameters */ |
| private Parameters parameters; |
| |
| /** Is this instance already disposed? */ |
| private volatile boolean isAlreadyDisposed; |
| |
| /** Is this instance currently disposed? */ |
| private volatile boolean isCurrentlyDisposing; |
| |
| /** The type of container where YAAFI is embedded */ |
| private String containerFlavour; |
| |
| /** |
| * The ms to wait before triggering a reconfiguration of the container or |
| * service |
| */ |
| private int reconfigurationDelay; |
| |
| /** The ms to wait before triggering a disposal of the container */ |
| private int disposalDelay; |
| |
| /** global flag for enabling/disabling dynamic proxies */ |
| private boolean hasDynamicProxies; |
| |
| /** The list of interceptor services applied to all services */ |
| private List<String> defaultInterceptorServiceList; |
| |
| /** The list of ServiceManagers as fallback service lookup */ |
| private List<String> fallbackServiceManagerList; |
| |
| /** |
| * the configuration for running the ComponentConfigurationPropertiesResolver |
| */ |
| private Configuration componentConfigurationPropertiesResolverConfig; |
| |
| /** |
| * Lock access during service initialization |
| */ |
| private final ReentrantLock serviceLock = new ReentrantLock(); |
| |
| ///////////////////////////////////////////////////////////////////////// |
| // Avalon Service Lifecycle |
| ///////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Constructor using sensible defaults. |
| */ |
| public ServiceContainerImpl() { |
| super(); |
| |
| this.containerFlavour = COMPONENT_CONTAINERFLAVOUR_VALUE; |
| this.componentRolesFlavour = COMPONENT_ROLECONFIGFLAVOUR_VALUE; |
| |
| this.componentRolesLocation = COMPONENT_ROLE_VALUE; |
| this.componentConfigurationLocation = COMPONENT_CONFIG_VALUE; |
| this.parametersLocation = COMPONENT_PARAMETERS_VALUE; |
| |
| this.isComponentConfigurationEncrypted = "false"; |
| this.isComponentRolesEncrypted = "false"; |
| this.isParametersEncrypted = "false"; |
| |
| this.isAlreadyDisposed = false; |
| this.isCurrentlyDisposing = false; |
| |
| this.serviceList = Collections.synchronizedList(new ArrayList<ServiceComponent>()); |
| this.serviceMap = new ConcurrentHashMap<String, ServiceComponent>(); |
| |
| this.applicationRootDir = new File(new File("").getAbsolutePath()); |
| this.tempRootDir = new File(System.getProperty("java.io.tmpdir", ".")); |
| |
| this.fallbackServiceManagerList = Collections.synchronizedList(new ArrayList<String>()); |
| this.defaultInterceptorServiceList = Collections.synchronizedList(new ArrayList<String>()); |
| |
| this.disposalDelay = DISPOSAL_DELAY_DEFAULT; |
| this.reconfigurationDelay = RECONFIGURATION_DELAY_DEFAULT; |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.logger.LogEnabled#enableLogging(org.apache.avalon.framework.logger.Logger) |
| */ |
| public void enableLogging(Logger logger) { |
| Validate.notNull(logger, "logger"); |
| this.logger = logger; |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.context.Contextualizable#contextualize(org.apache.avalon.framework.context.Context) |
| */ |
| public void contextualize(Context context) throws ContextException { |
| Validate.notNull(context, "context"); |
| // Argghhh - I need to to parse the Configuration before I can map the Context |
| this.callerContext = context; |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager) |
| */ |
| public void service(ServiceManager serviceManager) throws ServiceException { |
| this.parentServiceManager = serviceManager; |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration) |
| */ |
| public void configure(Configuration configuration) throws ConfigurationException { |
| Validate.notNull(configuration, "configuration"); |
| |
| // retrieve the reconfigurationDelay |
| |
| this.reconfigurationDelay = configuration.getChild(RECONFIGURATION_DELAY_KEY) |
| .getValueAsInteger(RECONFIGURATION_DELAY_DEFAULT); |
| |
| // retrieve the disposal delay |
| |
| this.disposalDelay = configuration.getChild(DISPOSAL_DELAY_KEY).getValueAsInteger(DISPOSAL_DELAY_DEFAULT); |
| |
| // evaluate if we are using dynamic proxies |
| |
| this.hasDynamicProxies = configuration.getChild(DYNAMICPROXY_ENABLED_KEY).getValueAsBoolean(false); |
| |
| // retrieve the container flavour |
| |
| this.setContainerFlavour( |
| configuration.getChild(CONTAINERFLAVOUR_CONFIG_KEY).getValue(COMPONENT_CONTAINERFLAVOUR_VALUE)); |
| |
| this.getLogger().debug("Using the following container type : " + this.getContainerFlavour()); |
| |
| // process the caller-supplied context here |
| |
| try { |
| // instantiate a mapper using the existing context - it might |
| // contain application specific entries we are not aware of |
| |
| AvalonToYaafiContextMapper mapper = new AvalonToYaafiContextMapper(this.getTempRootDir(), |
| this.callerContext, this.getClassLoader()); |
| |
| // do the magic mapping |
| |
| this.context = mapper.mapFrom(this.callerContext, this.getContainerFlavour()); |
| |
| // don't keep a reference of the caller-supplied context |
| |
| this.callerContext = null; |
| } catch (ContextException e) { |
| String msg = "Failed to parse the caller-supplied context"; |
| this.getLogger().error(msg, e); |
| throw new ConfigurationException(msg); |
| } |
| |
| // evaluate componentRoles |
| |
| Configuration currComponentRoles = configuration.getChild(COMPONENT_ROLE_KEYS); |
| |
| this.setComponentRolesLocation( |
| currComponentRoles.getChild(COMPONENT_LOCATION_KEY).getValue(COMPONENT_ROLE_VALUE)); |
| |
| this.setComponentRolesFlavour( |
| currComponentRoles.getChild(CONTAINERFLAVOUR_CONFIG_KEY).getValue(COMPONENT_ROLECONFIGFLAVOUR_VALUE)); |
| |
| this.setComponentRolesEncrypted(currComponentRoles.getChild(COMPONENT_ISENCRYPTED_KEY).getValue("false")); |
| |
| // evaluate componentConfiguraion |
| |
| Configuration currComponentConfiguration = configuration.getChild(COMPONENT_CONFIG_KEY); |
| |
| this.setComponentConfigurationLocation( |
| currComponentConfiguration.getChild(COMPONENT_LOCATION_KEY).getValue(COMPONENT_CONFIG_VALUE)); |
| |
| this.setComponentConfigurationEncrypted( |
| currComponentConfiguration.getChild(COMPONENT_ISENCRYPTED_KEY).getValue("false")); |
| |
| // get the configuration for componentConfigurationPropertiesResolver |
| |
| this.componentConfigurationPropertiesResolverConfig = configuration.getChild(COMPONENT_CONFIG_PROPERTIES_KEY); |
| |
| // evaluate parameters |
| |
| Configuration currParameters = configuration.getChild(COMPONENT_PARAMETERS_KEY); |
| |
| this.setParametersLocation( |
| currParameters.getChild(COMPONENT_LOCATION_KEY).getValue(COMPONENT_PARAMETERS_VALUE)); |
| |
| this.setParametersEncrypted(currParameters.getChild(COMPONENT_ISENCRYPTED_KEY).getValue("false")); |
| |
| // evaluate the default interceptors |
| |
| Configuration currInterceptorList = configuration.getChild(INTERCEPTOR_LIST_KEY); |
| |
| Configuration[] interceptorConfigList = currInterceptorList.getChildren(INTERCEPTOR_KEY); |
| |
| for (int i = 0; i < interceptorConfigList.length; i++) { |
| String interceptorServiceName = interceptorConfigList[i].getValue(null); |
| |
| if (!StringUtils.isEmpty(interceptorServiceName) && this.hasDynamicProxies()) { |
| this.defaultInterceptorServiceList.add(interceptorServiceName); |
| |
| this.getLogger().debug("Using the following default interceptor service : " + interceptorServiceName); |
| } |
| } |
| |
| // evaluate a list of service managers managing their own set of services |
| // independent from the Avalon container. This service managers are used |
| // to find services implemented as Spring bean or remote web services. |
| |
| Configuration currServiceManagerList = configuration.getChild(SERVICEMANAGER_LIST_KEY); |
| |
| Configuration[] serviceManagerConfigList = currServiceManagerList.getChildren(SERVICEMANAGER_KEY); |
| |
| for (int i = 0; i < serviceManagerConfigList.length; i++) { |
| String serviceManagerName = serviceManagerConfigList[i].getValue(null); |
| |
| if (!StringUtils.isEmpty(serviceManagerName)) { |
| this.fallbackServiceManagerList.add(serviceManagerName); |
| |
| this.getLogger().debug("Using the following fallback service manager : " + serviceManagerName); |
| } |
| } |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(org.apache.avalon.framework.parameters.Parameters) |
| */ |
| public void parameterize(Parameters parameters) throws ParameterException { |
| this.parameters = parameters; |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.activity.Initializable#initialize() |
| */ |
| public void initialize() throws Exception { |
| this.getLogger().debug("YAAFI Service Framework is starting up"); |
| |
| // set the directories being used |
| |
| this.setApplicationRootDir((File) this.getContext().get(AvalonYaafiConstants.URN_AVALON_HOME)); |
| |
| this.setTempRootDir((File) this.getContext().get(AvalonYaafiConstants.URN_AVALON_TEMP)); |
| |
| // get the configuration files |
| |
| this.roleConfiguration = loadConfiguration(this.componentRolesLocation, this.isComponentRolesEncrypted()); |
| |
| if (this.roleConfiguration == null) { |
| String msg = "Unable to locate the role configuration : " + this.componentRolesLocation; |
| this.getLogger().error(msg); |
| throw new ConfigurationException(msg); |
| } |
| |
| this.serviceConfiguration = loadConfiguration(this.componentConfigurationLocation, |
| this.isComponentConfigurationEncrypted()); |
| |
| // create the configuration properties |
| |
| Properties componentConfigurationProperties = this.loadComponentConfigurationProperties(); |
| |
| // expand the componentConfiguration using the componentConfigurationProperties |
| |
| ConfigurationUtil.expand(this.getLogger(), (DefaultConfiguration) this.serviceConfiguration, |
| componentConfigurationProperties); |
| |
| // create the default parameters |
| |
| if (this.getParameters() == null) { |
| this.parameters = this.loadParameters(this.parametersLocation, this.isParametersEncrypted()); |
| } |
| |
| // create the service implementation instances |
| |
| List<ServiceComponent> currServiceList = this.createServiceComponents(this.roleConfiguration, this.getLogger()); |
| |
| this.setServiceList(currServiceList); |
| |
| // fill the service map mapping from a service name to an instance |
| |
| for (int i = 0; i < this.getServiceList().size(); i++) { |
| ServiceComponent serviceComponent = (ServiceComponent) this.getServiceList().get(i); |
| this.getServiceMap().put(serviceComponent.getName(), serviceComponent); |
| } |
| |
| // ensure that fallback service managers are available |
| |
| for (int i = 0; i < this.fallbackServiceManagerList.size(); i++) { |
| String currServiceManagerName = (String) this.fallbackServiceManagerList.get(i); |
| if (this.getServiceMap().get(currServiceManagerName) == null) { |
| String msg = "The following fallback service manager was not found : " + currServiceManagerName; |
| throw new IllegalArgumentException(msg); |
| } |
| } |
| |
| // run the various lifecycle stages |
| |
| this.incarnateAll(this.getServiceList()); |
| |
| // we are up and running |
| |
| this.isCurrentlyDisposing = false; |
| this.isAlreadyDisposed = false; |
| this.getLogger().debug("YAAFI Avalon Service Container is up and running"); |
| } |
| |
| /** |
| * Disposes the service container implementation. |
| * |
| * @see org.apache.avalon.framework.activity.Disposable#dispose() |
| */ |
| public void dispose() { |
| if (this.isCurrentlyDisposing() || this.isAlreadyDisposed()) { |
| return; |
| } |
| |
| this.isCurrentlyDisposing = true; |
| |
| if (this.getLogger() != null) { |
| this.getLogger().debug("Disposing all services"); |
| } |
| |
| // wait some time before disposing all services |
| |
| waitForDisposal(); |
| |
| synchronized (this) { |
| // de-commission all services |
| |
| this.decommissionAll(this.getServiceList()); |
| |
| // dispose all services |
| |
| this.disposeAll(this.getServiceList()); |
| |
| // clean up |
| |
| this.getServiceList().clear(); |
| this.getServiceMap().clear(); |
| |
| this.componentRolesLocation = null; |
| this.componentConfigurationLocation = null; |
| this.context = null; |
| this.parametersLocation = null; |
| this.roleConfiguration = null; |
| this.serviceConfiguration = null; |
| this.parameters = null; |
| this.fallbackServiceManagerList = null; |
| this.defaultInterceptorServiceList = null; |
| this.isCurrentlyDisposing = false; |
| this.isAlreadyDisposed = true; |
| |
| if (this.getLogger() != null) { |
| this.getLogger().debug("All services are disposed"); |
| } |
| } |
| } |
| |
| /** |
| * Reconfiguring the services. I'm not sure how to implement this properly since |
| * the Avalon docs is vague on this subject. For now we suspend, reconfigure and |
| * resume the services in the correct order. |
| * |
| * @see org.apache.avalon.framework.configuration.Reconfigurable#reconfigure(org.apache.avalon.framework.configuration.Configuration) |
| */ |
| public synchronized void reconfigure(Configuration configuration) throws ConfigurationException { |
| Validate.notNull(configuration, "configuration"); |
| |
| int exceptionCounter = 0; |
| ServiceComponent serviceComponent; |
| |
| this.getLogger().warn("Reconfiguring all services ..."); |
| |
| // 1) wait for some time |
| |
| this.waitForReconfiguration(); |
| |
| // 2) store the new configuration |
| |
| this.serviceConfiguration = configuration; |
| |
| Properties componentConfigurationProperties = this.loadComponentConfigurationProperties(); |
| |
| ConfigurationUtil.expand(this.getLogger(), (DefaultConfiguration) this.serviceConfiguration, |
| componentConfigurationProperties); |
| |
| // 3) reconfigure the services |
| |
| for (int i = 0; i < this.getServiceList().size(); i++) { |
| serviceComponent = (ServiceComponent) this.getServiceList().get(i); |
| |
| Configuration serviceComponentConfiguration = this.serviceConfiguration |
| .getChild(serviceComponent.getShorthand()); |
| |
| try { |
| serviceComponent.setConfiguration(serviceComponentConfiguration); |
| serviceComponent.reconfigure(); |
| } catch (Throwable t) { |
| String msg = "Reconfiguring of " + serviceComponent.getShorthand() + " failed"; |
| this.getLogger().error(msg); |
| exceptionCounter++; |
| } |
| } |
| |
| // 4) check the result |
| |
| if (exceptionCounter > 0) { |
| String msg = "The reconfiguration failed with " + exceptionCounter + " exception(s)"; |
| this.getLogger().error(msg); |
| throw new ConfigurationException(msg); |
| } |
| } |
| |
| ///////////////////////////////////////////////////////////////////////// |
| // Server Interface Implementation |
| ///////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * @see org.apache.fulcrum.yaafi.framework.container.ServiceLifecycleManager#getRoleEntry(java.lang.String) |
| */ |
| public RoleEntry getRoleEntry(String name) throws ServiceException { |
| return this.getServiceComponentEx(name).getRoleEntry(); |
| } |
| |
| /** |
| * @see org.apache.fulcrum.yaafi.framework.container.ServiceLifecycleManager#getRoleEntries() |
| */ |
| public RoleEntry[] getRoleEntries() { |
| ServiceComponent serviceComponent; |
| List<ServiceComponent> serviceList = this.getServiceList(); |
| RoleEntry[] result = new RoleEntry[serviceList.size()]; |
| |
| for (int i = 0; i < result.length; i++) { |
| serviceComponent = (ServiceComponent) serviceList.get(i); |
| result[i] = serviceComponent.getRoleEntry(); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * @see org.apache.fulcrum.yaafi.framework.container.ServiceLifecycleManager#reconfigure(java.lang.String[]) |
| */ |
| public synchronized void reconfigure(String[] names) throws ServiceException, ConfigurationException { |
| Validate.notNull(names, "names"); |
| Validate.noNullElements(names, "names"); |
| |
| this.waitForReconfiguration(); |
| |
| for (int i = 0; i < names.length; i++) { |
| // ensure that the service exists since during our reconfiguration |
| // we might use a stale reconfiguration entry |
| |
| if (this.getServiceMap().get(names[i]) != null) { |
| this.reconfigure(names[i]); |
| } |
| } |
| } |
| |
| /** |
| * @see org.apache.avalon.framework.service.ServiceManager#hasService(java.lang.String) |
| */ |
| public boolean hasService(String name) { |
| Validate.notEmpty(name, "name"); |
| |
| boolean result; |
| |
| if (this.isCurrentlyDisposing() || this.isAlreadyDisposed()) { |
| return false; |
| } |
| |
| // look at our available service |
| result = this.getLocalServiceComponent(name) != null; |
| |
| // look at fallback service managers |
| if (!result) |
| result = this.hasFallbackService(name); |
| |
| // if we haven't found anything ask the parent ServiceManager |
| if (!result && this.hasParentServiceManager()) |
| result = this.getParentServiceManager().hasService(name); |
| |
| return result; |
| } |
| |
| /** |
| * Lookup a service instance. The implementation uses the following mechanism |
| * <ul> |
| * <li>look for a matching local service |
| * <li>use the fallback service manager as they might know the service |
| * <li>ask the parent service manager |
| * </ul> |
| * |
| * @see org.apache.avalon.framework.service.ServiceManager#lookup(java.lang.String) |
| */ |
| public Object lookup(String name) throws ServiceException { |
| Validate.notEmpty(name, "name"); |
| |
| Object result = null; |
| ServiceComponent serviceManagerComponent; |
| |
| if (this.isAlreadyDisposed()) |
| { |
| String msg = "The container is disposed an no services are available"; |
| this.getLogger().error(msg); |
| throw new ServiceException(name, msg); |
| } |
| |
| serviceLock.lock(); |
| try { |
| // 1) check our local services |
| serviceManagerComponent = this.getLocalServiceComponent(name); |
| |
| if (serviceManagerComponent != null) { |
| result = serviceManagerComponent.getInstance(); |
| |
| if (result != null && this.getLogger().isDebugEnabled()) { |
| String msg = "Located the service '" + name + "' in the local container"; |
| this.getLogger().debug(msg); |
| } |
| } |
| |
| // 2) look at fallback service managers |
| if (result == null) { |
| result = this.getFallbackService(name); |
| } |
| } catch (ServiceException e) |
| { |
| String msg = "Failed to lookup a service " + name; |
| this.getLogger().error(msg, e); |
| throw e; |
| } catch (Throwable t) |
| { |
| String msg = "Failed to lookup a service " + name; |
| this.getLogger().error(msg, t); |
| throw new ServiceException(name, msg, t); |
| } finally |
| { |
| serviceLock.unlock(); |
| } |
| |
| // 3) if we haven't found anything ask the parent ServiceManager |
| if (result == null && this.hasParentServiceManager()) |
| { |
| result = this.getParentServiceManager().lookup(name); |
| |
| if (result != null && this.getLogger().isDebugEnabled()) |
| { |
| String msg = "Located the service '" + name + "' using the parent service manager"; |
| this.getLogger().debug(msg); |
| } |
| } |
| |
| // if we still haven't found anything then complain |
| |
| if (result == null) |
| { |
| String msg = "The following component does not exist : " + name; |
| this.getLogger().error(msg); |
| throw new ServiceException(AvalonYaafiConstants.AVALON_CONTAINER_YAAFI, name); |
| } |
| |
| return result; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.apache.avalon.framework.service.ServiceManager#release(java.lang.Object) |
| */ |
| public void release(Object object) { |
| // AFAIK this is only useful for lifecycle management regarding |
| // lifestyle other than singleton. |
| } |
| |
| /** |
| * @see org.apache.fulcrum.yaafi.framework.container.ServiceContainer#decommission(java.lang.String) |
| */ |
| public synchronized void decommission(String name) throws ServiceException { |
| this.waitForReconfiguration(); |
| ServiceComponent serviceComponent = this.getServiceComponentEx(name); |
| this.decommission(serviceComponent); |
| } |
| |
| /** |
| * @see org.apache.fulcrum.yaafi.framework.container.ServiceContainer#getParameters() |
| */ |
| public Parameters getParameters() { |
| return this.parameters; |
| } |
| |
| ///////////////////////////////////////////////////////////////////////// |
| // Service Implementation |
| ///////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * @see java.lang.Object#toString() |
| */ |
| public String toString() { |
| ToStringBuilder toStringBuilder = new ToStringBuilder(this); |
| toStringBuilder.append("applicationRootDir", this.getApplicationRootDir()); |
| toStringBuilder.append("tempRootDir", this.getTempRootDir()); |
| toStringBuilder.append("componentRolesLocation", this.componentRolesLocation); |
| toStringBuilder.append("componentConfigurationLocation", this.componentConfigurationLocation); |
| toStringBuilder.append("parametersLocation", parametersLocation); |
| toStringBuilder.append("logger", this.getLogger().getClass().getName()); |
| toStringBuilder.append("hasDynamicProxies", this.hasDynamicProxies); |
| toStringBuilder.append("containerFlavour", this.containerFlavour); |
| toStringBuilder.append("componentRolesFlavour", this.componentRolesFlavour); |
| toStringBuilder.append("isComponentRolesEncrypted", this.isComponentRolesEncrypted); |
| toStringBuilder.append("isComponentConfigurationEncrypted", this.isComponentConfigurationEncrypted); |
| toStringBuilder.append("isParametersEncrypted", this.isParametersEncrypted); |
| |
| return toStringBuilder.toString(); |
| } |
| |
| /** |
| * Create a role configuration parser based on the container flavour. |
| * |
| * @return the role configuration parser |
| */ |
| private RoleConfigurationParser createRoleConfigurationParser() { |
| return new RoleConfigurationParserImpl(this.getComponentRolesFlavour()); |
| } |
| |
| /** |
| * Reconfigure a single service |
| * |
| * @param name the name of the service to be reconfigured |
| * @throws ServiceException the service was not found |
| * @throws ConfigurationException the reconfiguration failed |
| */ |
| private void reconfigure(String name) throws ServiceException, ConfigurationException { |
| Validate.notEmpty(name, "name"); |
| ServiceComponent serviceComponent = this.getServiceComponentEx(name); |
| |
| // reconfigure the component |
| |
| try { |
| serviceComponent.reconfigure(); |
| } catch (ConfigurationException e) { |
| String msg = "Reconfiguring failed : " + serviceComponent.getShorthand(); |
| this.getLogger().error(msg, e); |
| throw new ConfigurationException(msg, e); |
| } catch (Throwable t) { |
| String msg = "Reconfiguring failed : " + serviceComponent.getShorthand(); |
| this.getLogger().error(msg, t); |
| throw new ConfigurationException(msg, t); |
| } |
| } |
| |
| /** |
| * Enforce that a service is known to simplify error handling. |
| * |
| * @param name the name of the service component |
| * @return the service component |
| * @throws ServiceException the service was not found |
| */ |
| private ServiceComponent getServiceComponentEx(String name) throws ServiceException { |
| Validate.notEmpty(name, "name"); |
| ServiceComponent result = (ServiceComponent) this.getServiceMap().get(name); |
| |
| if (result == null) { |
| String msg = "The following component does not exist : " + name; |
| this.getLogger().error(msg); |
| throw new ServiceException(AvalonYaafiConstants.AVALON_CONTAINER_YAAFI, name); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Try to get a local service component. |
| * |
| * @param name the name of the service component |
| * @return the service component if any |
| */ |
| private ServiceComponent getLocalServiceComponent(String name) { |
| Validate.notEmpty(name, "name"); |
| ServiceComponent result = (ServiceComponent) this.getServiceMap().get(name); |
| return result; |
| } |
| |
| /** |
| * Try to get a service component provided by a fallback service manager. |
| * |
| * @param name the name of the service component |
| * @return the service component if any |
| * @throws Exception getting the service failed |
| */ |
| private Object getFallbackService(String name) throws Exception { |
| |
| Validate.notEmpty(name, "name"); |
| |
| Object result = null; |
| ServiceComponent serviceManagerComponent; |
| |
| for (int i = 0; i < this.fallbackServiceManagerList.size(); i++) { |
| String serviceManagerComponentName = (String) fallbackServiceManagerList.get(i); |
| serviceManagerComponent = this.getLocalServiceComponent(serviceManagerComponentName); |
| |
| if (serviceManagerComponent != null) { |
| ServiceManager currServiceManager = (ServiceManager) serviceManagerComponent.getInstance(); |
| |
| if (currServiceManager.hasService(name)) { |
| result = currServiceManager.lookup(name); |
| |
| if (result != null && this.getLogger().isDebugEnabled()) { |
| String msg = "Located the service '" + name + "' using the fallback service manager '" |
| + serviceManagerComponentName + "'"; |
| this.getLogger().debug(msg); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Try to get a service provided by a fallback service manager. |
| * |
| * @param name the name of the service component |
| * @return the service component if any |
| */ |
| private boolean hasFallbackService(String name) { |
| Validate.notEmpty(name, "name"); |
| |
| ServiceComponent serviceManagerComponent; |
| |
| for (int i = 0; i < this.fallbackServiceManagerList.size(); i++) { |
| String serviceManagerComponentName = (String) fallbackServiceManagerList.get(i); |
| serviceManagerComponent = this.getLocalServiceComponent(serviceManagerComponentName); |
| |
| if (serviceManagerComponent != null) { |
| ServiceManager currServiceManager; |
| |
| try { |
| currServiceManager = (ServiceManager) serviceManagerComponent.getInstance(); |
| if (currServiceManager.hasService(name)) { |
| return true; |
| } |
| } catch (Exception e) { |
| String msg = "Unable to invoke fallback service manager '" + serviceManagerComponentName + "'"; |
| this.getLogger().error(msg, e); |
| throw new RuntimeException(msg); |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * @param string The location of the component configuration file |
| */ |
| private void setComponentConfigurationLocation(String string) { |
| this.componentConfigurationLocation = string; |
| } |
| |
| /** |
| * @param string The location of the component role file |
| */ |
| private void setComponentRolesLocation(String string) { |
| this.componentRolesLocation = string; |
| } |
| |
| /** |
| * @param string The location of the parameters file |
| */ |
| private void setParametersLocation(String string) { |
| this.parametersLocation = string; |
| } |
| |
| /** |
| * @return The logger of the service container |
| */ |
| private Logger getLogger() { |
| return this.logger; |
| } |
| |
| /** |
| * @return Returns the serviceMap. |
| */ |
| private Map<String, ServiceComponent> getServiceMap() { |
| return this.serviceMap; |
| } |
| |
| /** |
| * Incarnation of a list of services. |
| * |
| * @param serviceList the list of available services |
| * @throws Exception the incarnation of a service failed |
| */ |
| private void incarnateAll(List<ServiceComponent> serviceList) throws Exception { |
| ServiceComponent serviceComponent; |
| |
| // configure all services |
| |
| for (int i = 0; i < serviceList.size(); i++) { |
| serviceComponent = (ServiceComponent) this.getServiceList().get(i); |
| this.configure(serviceComponent); |
| } |
| |
| // incarnate all services |
| |
| for (int i = 0; i < serviceList.size(); i++) { |
| serviceComponent = (ServiceComponent) this.getServiceList().get(i); |
| this.incarnate(serviceComponent); |
| } |
| |
| } |
| |
| /** |
| * Configure a single service component. After the invocation the service |
| * component is ready to be incarnated. |
| * |
| * @param serviceComponent The service component to be configured |
| * @throws Exception the configuration failed |
| */ |
| private void configure(ServiceComponent serviceComponent) throws Exception { |
| this.getLogger().debug("Configuring the service component " + serviceComponent.getShorthand()); |
| |
| // map the context according to the Avalon component type |
| |
| YaafiToAvalonContextMapper mapper = new YaafiToAvalonContextMapper(serviceComponent.getName(), |
| this.getClassLoader()); |
| |
| RoleEntry roleEntry = serviceComponent.getRoleEntry(); |
| String componentFlavour = roleEntry.getComponentFlavour(); |
| |
| DefaultContext serviceComponentContext = mapper.mapTo(this.getContext(), componentFlavour); |
| |
| // create the remaining Avalon artifacts for the service component |
| |
| Logger serviceComponentLogger = this.getLogger().getChildLogger(roleEntry.getLogCategory()); |
| |
| Configuration serviceComponentConfiguration = this.serviceConfiguration.getChild(roleEntry.getShorthand()); |
| |
| Parameters serviceComponentParameters = this.getParameters(); |
| |
| // configure the service component with all the artifacts |
| |
| serviceComponent.setLogger(serviceComponentLogger); |
| serviceComponent.setServiceManager(this); |
| serviceComponent.setContext(serviceComponentContext); |
| serviceComponent.setConfiguration(serviceComponentConfiguration); |
| serviceComponent.setParameters(serviceComponentParameters); |
| |
| // load the implementation class of the service |
| |
| serviceComponent.loadImplemtationClass(this.getClassLoader()); |
| } |
| |
| /** |
| * Incarnation of a configured service component. After the incarnation the |
| * service component is operational. |
| * |
| * @param serviceComponent The service component to incarnate |
| * @exception Exception incarnating the service component failed |
| */ |
| private void incarnate(ServiceComponent serviceComponent) throws Exception { |
| this.getLogger().debug("Incarnating the service " + serviceComponent.getShorthand()); |
| |
| serviceComponent.incarnate(); |
| } |
| |
| /** |
| * De-commission a ist of services. |
| * |
| * @param serviceList the list of services to decommission |
| */ |
| private void decommissionAll(List<ServiceComponent> serviceList) { |
| for (int i = serviceList.size() - 1; i >= 0; i--) { |
| ServiceComponent serviceComponent = (ServiceComponent) serviceList.get(i); |
| this.decommission(serviceComponent); |
| } |
| } |
| |
| /** |
| * Decommission of a single service component. Decommission consists of running |
| * the whole Avalon decommission lifecycle process for a service component. |
| * After decommission the service is not operational any longer. During |
| * decommissioning we ignore any exceptions since it is quite common that |
| * something goes wrong. |
| * |
| * @param serviceComponent The service component to decommission |
| */ |
| private void decommission(ServiceComponent serviceComponent) { |
| this.getLogger().debug("Decommission the service " + serviceComponent.getShorthand()); |
| |
| try { |
| serviceComponent.decommision(); |
| } catch (Throwable e) { |
| String msg = "Decommissioning the following service failed : " + serviceComponent.getName(); |
| this.getLogger().error(msg, e); |
| } |
| } |
| |
| /** |
| * Disposing a ist of services |
| * |
| * @param serviceList the list of services to dispose |
| */ |
| private void disposeAll(List<ServiceComponent> serviceList) { |
| for (int i = serviceList.size() - 1; i >= 0; i--) { |
| ServiceComponent serviceComponent = (ServiceComponent) serviceList.get(i); |
| this.dispose(serviceComponent); |
| } |
| } |
| |
| /** |
| * Disposing of a single service component. |
| * |
| * @param serviceComponent The service component to decommission |
| */ |
| private void dispose(ServiceComponent serviceComponent) { |
| this.getLogger().debug("Disposing the service " + serviceComponent.getShorthand()); |
| |
| try { |
| serviceComponent.dispose(); |
| } catch (Throwable e) { |
| String msg = "Disposing the following service failed : " + serviceComponent.getName(); |
| this.getLogger().error(msg, e); |
| } |
| } |
| |
| private boolean isCurrentlyDisposing() { |
| return isCurrentlyDisposing; |
| } |
| |
| private boolean isAlreadyDisposed() { |
| return isAlreadyDisposed; |
| } |
| |
| /** |
| * @return The list of currently know services |
| */ |
| private List<ServiceComponent> getServiceList() { |
| return this.serviceList; |
| } |
| |
| /** |
| * @param list The list of known services |
| */ |
| private void setServiceList(List<ServiceComponent> list) { |
| this.serviceList = list; |
| } |
| |
| /** |
| * Factory method for creating services. The service instances are not |
| * initialized at this point. |
| * |
| * @param roleConfiguration the role configuration file |
| * @param logger the logger |
| * @return the list of service components |
| * @throws ConfigurationException creating the service instance failed |
| */ |
| private List<ServiceComponent> createServiceComponents(Configuration roleConfiguration, Logger logger) |
| throws ConfigurationException { |
| Validate.notNull(roleConfiguration, "roleConfiguration"); |
| Validate.notNull(logger, "logger"); |
| |
| ArrayList<ServiceComponent> result = new ArrayList<ServiceComponent>(); |
| ServiceComponent serviceComponent = null; |
| |
| // create an appropriate instance of role configuration parser |
| |
| RoleConfigurationParser roleConfigurationParser = this.createRoleConfigurationParser(); |
| |
| // extract the role entries |
| |
| RoleEntry[] roleEntryList = roleConfigurationParser.parse(roleConfiguration); |
| |
| // get the default interceptors defined for the container |
| |
| List<String> defaultInterceptorList = this.getDefaultInterceptorServiceList(); |
| |
| // create the service components based on the role entries |
| |
| for (int i = 0; i < roleEntryList.length; i++) { |
| try { |
| // add the default interceptors to all role entries |
| |
| RoleEntry roleEntry = roleEntryList[i]; |
| |
| if (this.hasDynamicProxies()) { |
| roleEntry.addInterceptors(defaultInterceptorList); |
| } else { |
| roleEntry.setHasDynamicProxy(false); |
| } |
| |
| serviceComponent = new AvalonServiceComponentImpl(roleEntry, this.getLogger(), logger); |
| |
| result.add(serviceComponent); |
| } catch (Throwable t) { |
| String serviceComponentName = (serviceComponent != null ? serviceComponent.getName() : "unknown"); |
| String msg = "Failed to load the service " + serviceComponentName; |
| this.getLogger().error(msg, t); |
| throw new ConfigurationException(msg, t); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Load a configuration file either from a file or using the class loader. |
| * |
| * @param location the location of the file |
| * @param isEncrypted is the configuration encrypted |
| * @return The loaded configuration |
| * @throws Exception Something went wrong |
| */ |
| private Configuration loadConfiguration(String location, String isEncrypted) throws Exception { |
| Configuration result = null; |
| InputStreamLocator locator = this.createInputStreamLocator(); |
| InputStream is = locator.locate(location); |
| DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder(); |
| |
| if (is != null) { |
| try { |
| is = this.getDecryptingInputStream(is, isEncrypted); |
| result = builder.build(is); |
| } catch (Exception e) { |
| String msg = "Unable to parse the following file : " + location; |
| this.getLogger().error(msg, e); |
| throw e; |
| } finally { |
| safeClose(is); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Load a configuration property file either from a file or using the class |
| * loader. |
| * |
| * @return The loaded property file |
| * @throws ConfigurationException Something went wrong |
| */ |
| private Properties loadComponentConfigurationProperties() throws ConfigurationException { |
| Properties result; |
| ComponentConfigurationPropertiesResolver resolver; |
| |
| String className = this.componentConfigurationPropertiesResolverConfig.getChild("resolver") |
| .getValue(ComponentConfigurationPropertiesResolverImpl.class.getName()); |
| |
| try { |
| Class<?> resolverClass = this.getClassLoader().loadClass(className); |
| resolver = (ComponentConfigurationPropertiesResolver) resolverClass.newInstance(); |
| ContainerUtil.enableLogging(resolver, this.getLogger()); |
| ContainerUtil.contextualize(resolver, this.getContext()); |
| ContainerUtil.configure(resolver, this.componentConfigurationPropertiesResolverConfig); |
| |
| result = resolver.resolve(null); |
| |
| this.getLogger().debug("Using the following componentConfigurationProperties: " + result); |
| } catch (Exception e) { |
| String msg = "Resolving componentConfigurationProperties failed using the following class : " + className; |
| this.getLogger().error(msg, e); |
| throw new ConfigurationException(msg, e); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Load the parameters |
| * |
| * @param location The location as a file |
| * @param isEncrypted is the file encrypted |
| * @return The loaded configuration |
| * @throws Exception Something went wrong |
| */ |
| private Parameters loadParameters(String location, String isEncrypted) throws Exception { |
| InputStreamLocator locator = this.createInputStreamLocator(); |
| InputStream is = locator.locate(location); |
| Parameters result = new Parameters(); |
| |
| if (is != null) { |
| try { |
| is = this.getDecryptingInputStream(is, isEncrypted); |
| Properties props = new Properties(); |
| props.load(is); |
| result = Parameters.fromProperties(props); |
| } finally { |
| safeClose(is); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Creates a locator to find a resource either in the file system or in the |
| * classpath. |
| * |
| * @return the locator |
| */ |
| private InputStreamLocator createInputStreamLocator() { |
| return new InputStreamLocator(this.getApplicationRootDir(), this.getLogger()); |
| } |
| |
| /** |
| * Set the application directory of the container. |
| * |
| * @param dir The applicationRootDir to set. |
| */ |
| private void setApplicationRootDir(File dir) { |
| this.getLogger().debug("Setting applicationRootDir to " + dir.getAbsolutePath()); |
| |
| Validate.notNull(dir, "applicationRootDir is <null>"); |
| Validate.isTrue(dir.exists(), "applicationRootDir does not exist"); |
| |
| this.applicationRootDir = dir; |
| } |
| |
| /** |
| * @return Returns the applicationRootDir. |
| */ |
| private File getApplicationRootDir() { |
| return this.applicationRootDir; |
| } |
| |
| /** |
| * @return Returns the serviceManager of the parent container |
| */ |
| private ServiceManager getParentServiceManager() { |
| return this.parentServiceManager; |
| } |
| |
| /** |
| * @return is a parent ServiceManager available |
| */ |
| private boolean hasParentServiceManager() { |
| return this.getParentServiceManager() != null; |
| } |
| |
| /** |
| * Set the temporary directory of the container. |
| * |
| * @param dir The tempRootDir to set. |
| */ |
| private void setTempRootDir(File dir) { |
| this.getLogger().debug("Setting tempRootDir to " + dir.getAbsolutePath()); |
| |
| Validate.notNull(dir, "tempRootDir is <null>"); |
| Validate.isTrue(dir.exists(), "tempRootDir does not exist"); |
| Validate.isTrue(dir.canWrite(), "tempRootDir is not writeable"); |
| |
| this.tempRootDir = dir; |
| } |
| |
| /** |
| * @return Returns the tempRootDir. |
| */ |
| private File getTempRootDir() { |
| return tempRootDir; |
| } |
| |
| /** |
| * @return Returns the isComponentConfigurationEncrypted. |
| */ |
| private String isComponentConfigurationEncrypted() { |
| return isComponentConfigurationEncrypted; |
| } |
| |
| /** |
| * @param isComponentConfigurationEncrypted The |
| * isComponentConfigurationEncrypted to |
| * set. |
| */ |
| private void setComponentConfigurationEncrypted(String isComponentConfigurationEncrypted) { |
| this.isComponentConfigurationEncrypted = isComponentConfigurationEncrypted; |
| } |
| |
| /** |
| * @return Returns the isComponentRolesEncrypted. |
| */ |
| private String isComponentRolesEncrypted() { |
| return isComponentRolesEncrypted; |
| } |
| |
| /** |
| * @param isComponentRolesEncrypted The isComponentRolesEncrypted to set. |
| */ |
| private void setComponentRolesEncrypted(String isComponentRolesEncrypted) { |
| this.isComponentRolesEncrypted = isComponentRolesEncrypted; |
| } |
| |
| /** |
| * @return Returns the isParametersEncrypted. |
| */ |
| private String isParametersEncrypted() { |
| return isParametersEncrypted; |
| } |
| |
| /** |
| * @param isParametersEncrypted The isParametersEncrypted to set. |
| */ |
| private void setParametersEncrypted(String isParametersEncrypted) { |
| this.isParametersEncrypted = isParametersEncrypted; |
| } |
| |
| /** |
| * Create a decrypting input stream using the default password. |
| * |
| * @param is the input stream to be decrypted |
| * @param isEncrypted the encryption mode (true|false|auto) |
| * @return an decrypting input stream |
| * @throws Exception reading the input stream failed |
| */ |
| private InputStream getDecryptingInputStream(InputStream is, String isEncrypted) throws Exception { |
| return CryptoStreamFactory.getDecryptingInputStream(is, isEncrypted); |
| } |
| |
| /** |
| * @return Returns the containerFlavour. |
| */ |
| private String getContainerFlavour() { |
| return containerFlavour; |
| } |
| |
| /** |
| * @param containerFlavour The containerFlavour to set. |
| */ |
| private void setContainerFlavour(String containerFlavour) { |
| this.containerFlavour = containerFlavour; |
| } |
| |
| /** |
| * @return Returns the componentRolesFlavour. |
| */ |
| private String getComponentRolesFlavour() { |
| return componentRolesFlavour; |
| } |
| |
| /** |
| * @param componentRolesFlavour The componentRolesFlavour to set. |
| */ |
| private void setComponentRolesFlavour(String componentRolesFlavour) { |
| this.componentRolesFlavour = componentRolesFlavour; |
| } |
| |
| /** |
| * @return Returns the context. |
| */ |
| private Context getContext() { |
| return context; |
| } |
| |
| /** |
| * @return Returns the hasDynamicProxies. |
| */ |
| private boolean hasDynamicProxies() { |
| return this.hasDynamicProxies; |
| } |
| |
| /** |
| * @return Returns the defaultInterceptorServiceList. |
| */ |
| private List<String> getDefaultInterceptorServiceList() { |
| return defaultInterceptorServiceList; |
| } |
| |
| /** |
| * @return the containers class loader |
| */ |
| private ClassLoader getClassLoader() { |
| return this.getClass().getClassLoader(); |
| } |
| |
| /** |
| * Wait for the time configured as 'reconfigurationDelay' before reconfiguring |
| * the container or services. |
| */ |
| private void waitForReconfiguration() { |
| try { |
| Thread.sleep(this.reconfigurationDelay); |
| } catch (InterruptedException e) { |
| // nothing to do |
| } |
| } |
| |
| /** |
| * Wait for the time configured as 'disposalDelay' before disposing the |
| * container. |
| */ |
| private void waitForDisposal() { |
| try { |
| Thread.sleep(this.disposalDelay); |
| } catch (InterruptedException e) { |
| // nothing to do |
| } |
| } |
| |
| private void safeClose(InputStream is) { |
| if (is != null) { |
| try { |
| is.close(); |
| } catch (Exception e) { |
| getLogger().error("Failed to close an input stream", e); |
| } |
| } |
| } |
| } |