/* * Copyright (C) 2005-2013 Alfresco Software Limited. * * This file is part of Alfresco * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . */ package org.alfresco.repo.management.subsystems; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.util.PropertyPlaceholderHelper; /** * A base class for {@link PropertyBackedBean}s. Gets its category from its Spring bean name and automatically * propagates and resolves property defaults on initialization. Automatically destroys itself on server shutdown. * Communicates its creation and destruction and start and stop events to a {@link PropertyBackedBeanRegistry}. Listens * for start and stop events from remote nodes in order to keep the bean in sync with edits made on a remote node. On * receiving a start event from a remote node, the bean is completely reinitialized, allowing it to be resynchronized * with any persisted changes. * * @author dward */ public abstract class AbstractPropertyBackedBean implements PropertyBackedBean, ApplicationContextAware, ApplicationListener, InitializingBean, DisposableBean, BeanNameAware { /** * When true, calls to setProperties / setProperty are persisted to the MBean if it exists. */ private boolean saveSetProperty = false; /** The default final part of an ID. */ protected static final String DEFAULT_INSTANCE_NAME = "default"; /** The parent application context. */ private ApplicationContext parent; /** The registry of all property backed beans. */ private PropertyBackedBeanRegistry registry; /** The category (first part of the ID). */ private String category; /** The bean name if we have been initialized by Spring. */ private String beanName; /** The hierarchical instance path within the category (second part of the ID). */ private List instancePath = Collections.singletonList(AbstractPropertyBackedBean.DEFAULT_INSTANCE_NAME); /** The combined unique id. */ private List id; /** Should the application context be started on startup of the parent application?. */ private boolean autoStart; /** Property defaults provided by the installer or System properties. */ private Properties propertyDefaults; /** Resolves placeholders in the property defaults. */ private DefaultResolver defaultResolver = new DefaultResolver(); /** The lifecycle states. */ protected enum RuntimeState {UNINITIALIZED, STOPPED, PENDING_BROADCAST_START, STARTED}; protected RuntimeState runtimeState = RuntimeState.UNINITIALIZED; /** The state. */ private PropertyBackedBeanState state; /** Lock for concurrent access. */ protected ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); /** * Used in conjunction with {@link #localSetProperties} to control setting of * properties from either a JMX client or by code in the local Alfresco * node calling {@link AbstractPropertyBackedBean#setProperties(Map)} or * {@link AbstractPropertyBackedBean#setProperty(String, String)}. * Is true when there is a nested call to either of these * methods. This is the case when there is an MBean AND one of these method was * NOT originally called from that MBean (it is a local code). */ private ThreadLocal nestedCall = new ThreadLocal() { @Override protected Boolean initialValue() { return false; } }; /** * Used in conjunction with {@link #nestedCall} to control setting of * properties from either a JMX client or by code in the local Alfresco * node calling {@link AbstractPropertyBackedBean#setProperties(Map)} or * {@link AbstractPropertyBackedBean#setProperty(String, String)}. * Is set to true when there is a nested call back from * a JMX bean. */ private ThreadLocal localSetProperties = new ThreadLocal() { @Override protected Boolean initialValue() { return false; } }; /** The logger. */ private static Log logger = LogFactory.getLog(AbstractPropertyBackedBean.class); /* * (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context. * ApplicationContext) */ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.parent = applicationContext; } /** * Sets the registry of all property backed beans. * * @param registry * the registry of all property backed beans */ public void setRegistry(PropertyBackedBeanRegistry registry) { this.registry = registry; } /** * Gets the registry of all property backed beans. * * @return the registry of all property backed beans */ protected PropertyBackedBeanRegistry getRegistry() { return this.registry; } /* * (non-Javadoc) * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) */ public void setBeanName(String name) { this.beanName = name; } /** * Sets the category (first part of the ID). * * @param category * the category */ public void setCategory(String category) { this.category = category; } /** * Sets the hierarchical instance path within the category (second part of the ID).. * * @param instancePath * the instance path */ public void setInstancePath(List instancePath) { this.instancePath = instancePath; } /** * Indicates whether the bean should be started on startup of the parent application context. * * @param autoStart * true if the bean should be started on startup of the parent application context */ public void setAutoStart(boolean autoStart) { this.autoStart = autoStart; } /** * Sets the property defaults provided by the installer or System properties. * * @param propertyDefaults * the property defaults */ public void setPropertyDefaults(Properties propertyDefaults) { this.propertyDefaults = propertyDefaults; } /** * Gets the property defaults provided by the installer or System properties. * * @return the property defaults */ protected Properties getPropertyDefaults() { return this.propertyDefaults; } /** * Resolves the default value of a property, if there is one, expanding placholders as necessary. * * @param name * the property name * @return the resolved default value or null if there isn't one */ protected String resolveDefault(String name) { String value = this.propertyDefaults.getProperty(name); if (value != null) { value = this.defaultResolver.resolveValue(value); } return value; } /** * Gets the parent application context. * * @return the parent application context */ protected ApplicationContext getParent() { return this.parent; } /** * Gets the state. * * @param start * are we making use of the state? I.e. should we start it if it has not been already? * @return the state */ protected PropertyBackedBeanState getState(boolean start) { if (start) { start(true, false); } return this.state; } /** * When set to true, calls to setProperties / setProperty are persisted to the MBean if it exists. */ public void setSaveSetProperty(boolean saveSetProperty) { this.saveSetProperty = saveSetProperty; } /** * {@inheritDoc} */ public void afterPropertiesSet() throws Exception { // Default the category to the bean name if (this.category == null) { if (this.beanName == null) { throw new IllegalStateException("Category not provided"); } this.category = this.beanName; } // Derive the unique ID from the category and instance path List path = getInstancePath(); this.id = new ArrayList(path.size() + 1); this.id.add(this.category); this.id.addAll(getInstancePath()); init(); } /** * Initializes or resets the bean and its state. */ public final void init() { this.lock.writeLock().lock(); try { doInit(); } finally { this.lock.writeLock().unlock(); } } /** * Initializes or resets the bean and its state. */ protected void doInit() { boolean hadWriteLock = this.lock.isWriteLockedByCurrentThread(); if (this.runtimeState == RuntimeState.UNINITIALIZED) { if (!hadWriteLock) { this.lock.readLock().unlock(); this.lock.writeLock().lock(); } try { if (this.runtimeState == RuntimeState.UNINITIALIZED) { logger.debug("doInit() createInitialState"); this.state = createInitialState(); logger.debug("doInit() applyDefaultOverrides "+state); applyDefaultOverrides(this.state); this.runtimeState = RuntimeState.STOPPED; logger.debug("doInit() register"); this.registry.register(this); logger.debug("doInit() done"); } } catch (IOException e) { throw new RuntimeException(e); } finally { logger.debug("doInit() state="+runtimeState); if (!hadWriteLock) { this.lock.readLock().lock(); this.lock.writeLock().unlock(); } } } } /** * {@inheritDoc} */ public final void revert() { this.lock.writeLock().lock(); try { stop(true); destroy(true); doInit(); } finally { this.lock.writeLock().unlock(); } } /** * Creates the initial state. * * @return the property backed bean state * @throws IOException * Signals that an I/O exception has occurred. */ protected abstract PropertyBackedBeanState createInitialState() throws IOException; /** * Applies default overrides to the initial state. * * @param state * the state * @throws IOException * Signals that an I/O exception has occurred. */ protected void applyDefaultOverrides(PropertyBackedBeanState state) throws IOException { for (String name : state.getPropertyNames()) { String override = resolveDefault(name); if (override != null) { state.setProperty(name, override); } } } /** * {@inheritDoc} */ public List getId() { return this.id; } /** * Gets the category. * * @return the category */ protected String getCategory() { return this.category; } /** * Gets the hierarchical instance path within the category (second part of the ID). * * @return the instance path */ protected List getInstancePath() { return this.instancePath; } /** * {@inheritDoc} */ public final void destroy() { this.lock.writeLock().lock(); try { destroy(false); } finally { this.lock.writeLock().unlock(); } } /** * Releases any resources held by this component. * * @param isPermanent * is the component being destroyed forever, i.e. should persisted values be removed? On server shutdown, * this value would be false, whereas on the removal of a dynamically created instance, this * value would be true. */ protected void destroy(boolean isPermanent) { if (this.runtimeState != RuntimeState.UNINITIALIZED) { boolean hadWriteLock = this.lock.isWriteLockedByCurrentThread(); if (!hadWriteLock) { this.lock.readLock().unlock(); this.lock.writeLock().lock(); } try { if (this.runtimeState != RuntimeState.UNINITIALIZED) { logger.debug("destroy() stop state="+runtimeState); stop(false); logger.debug("destroy() deregister "+isPermanent); this.registry.deregister(this, isPermanent); this.state = null; this.runtimeState = RuntimeState.UNINITIALIZED; logger.debug("destroy() done"); } } finally { logger.debug("destroy() state="+runtimeState); if (!hadWriteLock) { this.lock.readLock().lock(); this.lock.writeLock().unlock(); } } } } /** * {@inheritDoc} */ public boolean isUpdateable(String name) { return true; } /** * {@inheritDoc} */ public String getDescription(String name) { return isUpdateable(name) ? "Editable Property " + name : "Read-only Property " + name; } /** * {@inheritDoc} */ public void onApplicationEvent(ApplicationEvent event) { if (this.autoStart && event instanceof ContextRefreshedEvent && event.getSource() == this.parent) { this.lock.writeLock().lock(); try { start(false, false); } catch (Exception e) { // Let's log and swallow auto-start exceptions so that they are non-fatal. This means that the system // can hopefully be brought up to a level where its configuration can be edited and corrected logger.error("Error auto-starting subsystem", e); } finally { this.lock.writeLock().unlock(); } } else if (event instanceof PropertyBackedBeanStartedEvent) { this.lock.writeLock().lock(); try { // If we aren't started, reinitialize so that we pick up state changes from the database switch (this.runtimeState) { case PENDING_BROADCAST_START: case STOPPED: destroy(false); // fall through case UNINITIALIZED: start(false, false); } } finally { this.lock.writeLock().unlock(); } } else if (event instanceof PropertyBackedBeanStoppedEvent) { this.lock.writeLock().lock(); try { // Completely destroy the state so that it will have to be reinitialized should the bean be put back in // to use by this node destroy(false); } finally { this.lock.writeLock().unlock(); } } } /** * {@inheritDoc} */ public String getProperty(String name) { this.lock.readLock().lock(); try { doInit(); return this.state.getProperty(name); } finally { this.lock.readLock().unlock(); } } /** * {@inheritDoc} */ public Set getPropertyNames() { this.lock.readLock().lock(); try { doInit(); return this.state.getPropertyNames(); } finally { this.lock.readLock().unlock(); } } private void setPropertyInternal(String name, String value) { // Bring down the bean. The caller may have already broadcast this across the cluster stop(false); doInit(); this.state.setProperty(name, value); } private void setPropertiesInternal(Map properties) { // Bring down the bean. The caller may have already broadcast this across the cluster stop(false); doInit(); Map previousValues = new HashMap(properties.size() * 2); try { // Set each of the properties and back up their previous values just in case for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); String previousValue = this.state.getProperty(property); this.state.setProperty(property, entry.getValue()); previousValues.put(property, previousValue); } // Attempt to start locally start(false, true); // We still haven't broadcast the start - a persist is required first so this will be done by the caller } catch (Exception e) { // Oh dear - something went wrong. So restore previous state before rethrowing for (Map.Entry entry : previousValues.entrySet()) { this.state.setProperty(entry.getKey(), entry.getValue()); } // Bring the bean back up across the cluster start(true, false); if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new IllegalStateException(e); } } /** * {@inheritDoc} * *

When called from code within the local node the values are saved to the * database in the Enterprise edition and will be visible in a JMX client.

* * @param name * @param value */ public void setProperty(String name, String value) { if (logger.isDebugEnabled()) { logger.debug("setProperty("+name+','+value+")"); } if (!nestedCall.get()) { nestedCall.set(true); this.lock.writeLock().lock(); try { boolean mBeanInfoChange = !getPropertyNames().contains(name); // When setting properties locally AND there is an MBean, the following broadcast // results in a call to the MBean's setAttributes method, which in turn results // in a nested call back. The call back sets the values in this bean and // localSetProperties will be set to true. The MBean persists the changes and the // broadcast method returns. If there is no MBean (community edition) OR when // initiated from the MBean (say setting a value via JConsole), nothing happens // as a result of the broadcast. if (saveSetProperty) { logger.debug("setProperty() broadcastSetProperties"); this.registry.broadcastSetProperty(this, name, value); } if (localSetProperties.get()) { if (mBeanInfoChange) { // Re register the bean so new properties are visible in JConsole which does // not check MBeanInfo for changes otherwise. logger.debug("setProperty() destroy"); // Commented out to avoid "UserTransaction is not visible from class loader" as it drops the context. // So we have to just live with the JConsole as it is. // destroy(false); // Attempt to start locally start(false, true); } } else { logger.debug("setProperty() setPropertyInternal"); setPropertyInternal(name, value); } } finally { localSetProperties.set(false); nestedCall.set(false); this.lock.writeLock().unlock(); } } else { // A nested call indicates there is a MBean and that this method was // NOT originally called from that MBean. localSetProperties.set(true); logger.debug("setProperty() callback setPropertyInternal"); setPropertyInternal(name, value); } } /** * {@inheritDoc} * *

When called from code within the local node the values are saved to the * database in the Enterprise edition and will be visible in a JMX client.

* * @param properties to be saved. */ public void setProperties(Map properties) { if (logger.isDebugEnabled()) { logger.debug("setProperties("+properties+")"); } if (!nestedCall.get()) { nestedCall.set(true); boolean hadWriteLock = this.lock.isWriteLockedByCurrentThread(); if (!hadWriteLock) { this.lock.writeLock().lock(); } try { boolean mBeanInfoChange = !getPropertyNames().containsAll(properties.keySet()); // When setting properties locally AND there is an MBean, the following broadcast // results in a call to the MBean's setAttributes method, which in turn results // in a nested call back. The call back sets the values in this bean and // localSetProperties will be set to true. The MBean persists the changes and the // broadcast method returns. If there is no MBean (community edition) OR when // initiated from the MBean (say setting a value via JConsole), nothing happens // as a result of the broadcast. if (saveSetProperty) { logger.debug("setProperties() broadcastSetProperties"); this.registry.broadcastSetProperties(this, properties); } if (localSetProperties.get()) { if (mBeanInfoChange) { // Re register the bean so new properties are visible in JConsole which does // not check MBeanInfo for changes otherwise. logger.debug("setProperties() destroy"); // Commented out to avoid "UserTransaction is not visible from class loader" as it drops the context. // So we have to just live with the JConsole as it is. // destroy(false); // Attempt to start locally start(true, false); } } else { logger.debug("setProperties() setPropertiesInternal"); setPropertiesInternal(properties); } } finally { localSetProperties.set(false); nestedCall.set(false); if (!hadWriteLock) { this.lock.writeLock().unlock(); } } } else { // A nested call indicates there is a MBean and that this method was // NOT originally called from that MBean. localSetProperties.set(true); logger.debug("setProperties() callback setPropertiesInternal"); setPropertiesInternal(properties); } } /** * Removes a property added by code within the local node. * * @param propertyNames to be removed. */ public void removeProperty(String name) { removeProperties(Collections.singleton(name)); } /** * Removes properties added by code within the local node. * * @param propertyNames to be removed. */ public void removeProperties(Collection properties) { if (logger.isDebugEnabled()) { logger.debug("removeProperties("+properties+")"); } if (!nestedCall.get()) { nestedCall.set(true); boolean hadWriteLock = this.lock.isWriteLockedByCurrentThread(); if (!hadWriteLock) { this.lock.writeLock().lock(); } try { boolean mBeanInfoChange = !getPropertyNames().containsAll(properties); // When setting properties locally AND there is an MBean, the following broadcast // results in a call to the MBean's setAttributes method, which in turn results // in a nested call back. The call back sets the values in this bean and // localSetProperties will be set to true. The MBean persists the changes and the // broadcast method returns. If there is no MBean (community edition) OR when // initiated from the MBean (say setting a value via JConsole), nothing happens // as a result of the broadcast. if (saveSetProperty) { logger.debug("removeProperties() broadcastRemoveProperties"); this.registry.broadcastRemoveProperties(this, properties); } if (localSetProperties.get()) { if (mBeanInfoChange) { // Re register the bean so new properties are visible in JConsole which does // not check MBeanInfo for changes otherwise. logger.debug("removeProperties() destroy"); // Commented out to avoid "UserTransaction is not visible from class loader" as it drops the context. // So we have to just live with the JConsole as it is. // destroy(false); // Attempt to start locally start(true, false); } } else { logger.debug("removeProperties() removePropertiesInternal"); removePropertiesInternal(properties); } } finally { localSetProperties.set(false); nestedCall.set(false); if (!hadWriteLock) { this.lock.writeLock().unlock(); } } } else { // A nested call indicates there is a MBean and that this method was // NOT originally called from that MBean. localSetProperties.set(true); logger.debug("removeProperties() callback removePropertiesInternal"); removePropertiesInternal(properties); } } private void removePropertiesInternal(Collection properties) { // Bring down the bean. The caller may have already broadcast this across the cluster stop(false); doInit(); Map previousValues = new HashMap(properties.size() * 2); try { // Set each of the properties and back up their previous values just in case for (String property : properties) { String previousValue = state.getProperty(property); this.state.removeProperty(property); previousValues.put(property, previousValue); } // Attempt to start locally start(false, true); // We still haven't broadcast the start - a persist is required first so this will be done by the caller } catch (Exception e) { // Oh dear - something went wrong. So restore previous state before rethrowing for (Map.Entry entry : previousValues.entrySet()) { this.state.setProperty(entry.getKey(), entry.getValue()); } // Bring the bean back up across the cluster start(true, false); if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new IllegalStateException(e); } } /** * {@inheritDoc} */ public final void start() { this.lock.writeLock().lock(); try { start(true, false); } finally { this.lock.writeLock().unlock(); } } /** * Starts the bean, optionally broadcasting the event to remote nodes. * * @param broadcastNow * Should the event be broadcast immediately? * @param broadcastLater * Should the event be broadcast ever? */ protected void start(boolean broadcastNow, boolean broadcastLater) { boolean hadWriteLock = this.lock.isWriteLockedByCurrentThread(); if (this.runtimeState != RuntimeState.STARTED) { if (!hadWriteLock) { this.lock.readLock().unlock(); this.lock.writeLock().lock(); } try { switch (this.runtimeState) { case UNINITIALIZED: doInit(); // fall through case STOPPED: this.state.start(); this.runtimeState = broadcastLater ? RuntimeState.PENDING_BROADCAST_START : RuntimeState.STARTED; // fall through case PENDING_BROADCAST_START: if (broadcastNow) { this.registry.broadcastStart(this); this.runtimeState = RuntimeState.STARTED; } } } finally { if (!hadWriteLock) { this.lock.readLock().lock(); this.lock.writeLock().unlock(); } } } } /** * {@inheritDoc} */ public final void stop() { this.lock.writeLock().lock(); try { stop(true); } finally { this.lock.writeLock().unlock(); } } /** * Stops the bean, optionally broadcasting the event to remote nodes. * * @param broadcast * Should the event be broadcast? */ protected void stop(boolean broadcast) { boolean hadWriteLock = this.lock.isWriteLockedByCurrentThread(); switch (this.runtimeState) { case PENDING_BROADCAST_START: case STARTED: if (!hadWriteLock) { this.lock.readLock().unlock(); this.lock.writeLock().lock(); } try { switch (this.runtimeState) { case STARTED: if (broadcast) { this.registry.broadcastStop(this); } // fall through case PENDING_BROADCAST_START: this.state.stop(); this.runtimeState = RuntimeState.STOPPED; } } finally { if (!hadWriteLock) { this.lock.readLock().lock(); this.lock.writeLock().unlock(); } } } } /** * Uses a Spring {@link PropertyPlaceholderHelper} to resolve placeholders in the property defaults. This means * that placeholders need not be displayed in the configuration UI or JMX console. */ public class DefaultResolver extends PropertyPlaceholderHelper { /** * Instantiates a new default resolver. */ public DefaultResolver() { super("${", "}", ":", true); } /** * Expands the given value, resolving any ${} placeholders using the property defaults. * * @param val * the value to expand * @return the expanded value */ public String resolveValue(String val) { return AbstractPropertyBackedBean.this.propertyDefaults == null ? null : replacePlaceholders( val, AbstractPropertyBackedBean.this.propertyDefaults); } } }