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);
- }
- }
-
+/*
+ * 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.alfresco.error.AlfrescoRuntimeException;
+
+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;
+
+ /** Property defaults provided by the JASYPT decryptor. */
+ private Properties encryptedPropertyDefaults;
+
+ /** Resolves placeholders in the property defaults. */
+ private DefaultResolver defaultResolver = new DefaultResolver();
+ private LoggableErrorEvent loggableErrorEvent;
+
+ /** The lifecycle states. */
+ protected enum RuntimeState {UNINITIALIZED, STOPPED, PENDING_BROADCAST_START, STARTED};
+
+ protected RuntimeState runtimeState = RuntimeState.UNINITIALIZED;
+
+ /** The state. */
+ private PropertyBackedBeanState state;
+
+ private int timeoutForLoggableErrorEventMonitoringThread = 60000;
+ private int incrementForLoggableErrorEventMonitoringThread = 100;
+
+ /** Lock for concurrent access. */
+ protected ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+ private String loggableErrorEventMatcher = "";
+
+ public void setLoggableErrorEventMatcher(String loggableErrorEventMatcher)
+ {
+ this.loggableErrorEventMatcher = loggableErrorEventMatcher;
+ }
+
+ public void setTimeoutForLoggableErrorEventMonitoringThread(int timeout)
+ {
+ timeoutForLoggableErrorEventMonitoringThread = timeout;
+ }
+
+ public void setIncrementForLoggableErrorEventMonitoringThread(int increment)
+ {
+ incrementForLoggableErrorEventMonitoringThread = increment;
+ }
+
+ /**
+ * 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;
+ }
+
+ public void setEncryptedPropertyDefaults(Properties propertyDefaults)
+ {
+ this.encryptedPropertyDefaults = 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)
+ {
+ Properties props = new Properties();
+
+ if(propertyDefaults != null)
+ {
+ for( Object key : propertyDefaults.keySet())
+ {
+ props.setProperty((String)key, propertyDefaults.getProperty((String)key));
+ }
+ }
+
+ if(encryptedPropertyDefaults != null)
+ {
+ for( Object key : encryptedPropertyDefaults.keySet())
+ {
+ props.setProperty((String)key, encryptedPropertyDefaults.getProperty((String)key));
+ }
+ }
+
+ String value = props.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();
+ }
+ }
+ else if (event instanceof LoggableErrorEvent)
+ {
+ Object source = event.getSource();
+ if ((source != null) && source.getClass().getName().matches(loggableErrorEventMatcher))
+ {
+ this.lock.writeLock().lock();
+
+ try
+ {
+ this.loggableErrorEvent = (LoggableErrorEvent) event;
+ }
+ 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.
*
@@ -851,9 +890,9 @@ public abstract class AbstractPropertyBackedBean implements PropertyBackedBean,
*/
public void removeProperty(String name)
{
- removeProperties(Collections.singleton(name));
- }
-
+ removeProperties(Collections.singleton(name));
+ }
+
/**
* Removes properties added by code within the local node.
*
@@ -861,285 +900,359 @@ public abstract class AbstractPropertyBackedBean implements PropertyBackedBean,
*/
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)
- {
- Properties props = new Properties();
-
- if(propertyDefaults != null)
- {
- for( Object key : propertyDefaults.keySet())
- {
- props.setProperty((String)key, propertyDefaults.getProperty((String)key));
- }
- }
-
- if(encryptedPropertyDefaults != null)
- {
- for( Object key : encryptedPropertyDefaults.keySet())
- {
- props.setProperty((String)key, encryptedPropertyDefaults.getProperty((String)key));
- }
- }
- return replacePlaceholders(val, props);
- }
-
- }
-}
+ 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);
+ }
+ }
+
+ private class LoggableErrorEventMonitoringThread extends Thread
+ {
+ private int counter = 0;
+ private AlfrescoRuntimeException caughtException = null;
+
+ public void run()
+ {
+ while (true)
+ {
+ AbstractPropertyBackedBean.this.lock.readLock().lock();
+
+ try
+ {
+ if (loggableErrorEvent != null)
+ {
+ caughtException = loggableErrorEvent.getException();
+ }
+
+ }
+ finally
+ {
+ AbstractPropertyBackedBean.this.lock.readLock().unlock();
+ }
+
+ if (counter >= timeoutForLoggableErrorEventMonitoringThread || caughtException != null)
+ {
+ break;
+ }
+
+ try
+ {
+ Thread.sleep(incrementForLoggableErrorEventMonitoringThread);
+ counter += incrementForLoggableErrorEventMonitoringThread;
+ }
+ catch (InterruptedException ie)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(ie.toString());
+ }
+ }
+ }
+
+ if (caughtException != null)
+ {
+ if (logger.isErrorEnabled())
+ {
+ logger.error(caughtException.toString());
+ }
+
+ loggableErrorEvent = null;
+ throw caughtException;
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public final void start()
+ {
+ this.lock.writeLock().lock();
+
+ try
+ {
+ start(true, false);
+
+ if (loggableErrorEvent != null)
+ {
+ AlfrescoRuntimeException caughtException = loggableErrorEvent.getException();
+
+ if (logger.isErrorEnabled())
+ {
+ logger.error(caughtException.toString());
+ }
+
+ loggableErrorEvent = null;
+ throw caughtException;
+ }
+ else
+ {
+ (new LoggableErrorEventMonitoringThread()).start();
+ }
+ }
+ 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)
+ {
+ Properties props = new Properties();
+
+ if(propertyDefaults != null)
+ {
+ for( Object key : propertyDefaults.keySet())
+ {
+ props.setProperty((String)key, propertyDefaults.getProperty((String)key));
+ }
+ }
+
+ if(encryptedPropertyDefaults != null)
+ {
+ for( Object key : encryptedPropertyDefaults.keySet())
+ {
+ props.setProperty((String)key, encryptedPropertyDefaults.getProperty((String)key));
+ }
+ }
+ return replacePlaceholders(val, props);
+ }
+
+ }
+}
diff --git a/source/java/org/alfresco/repo/management/subsystems/LoggableErrorEvent.java b/source/java/org/alfresco/repo/management/subsystems/LoggableErrorEvent.java
new file mode 100644
index 0000000000..67fe6cf66a
--- /dev/null
+++ b/source/java/org/alfresco/repo/management/subsystems/LoggableErrorEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005-2015 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 org.alfresco.error.AlfrescoRuntimeException;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * Alfresco custom ApplicationEvent class used for publishing errors in subsystems.
+ */
+public class LoggableErrorEvent extends ApplicationEvent
+{
+ private static final String UNSPECIFIED_ERROR_MESSAGE = "system.loggable_error_event.unspecified_error";
+
+ private AlfrescoRuntimeException exception;
+
+ /**
+ * Create a new LoggableErrorEvent.
+ * @param source the component that published the event (never {@code null})
+ * @param exception the error to publish
+ */
+ public LoggableErrorEvent(Object source, AlfrescoRuntimeException exception)
+ {
+ super(source);
+ this.exception = exception;
+ }
+
+ /**
+ * Get this LoggableErrorEvent's exception.
+ * @return the stored exception if not null, otherwise a new RuntimeException
+ */
+ public AlfrescoRuntimeException getException()
+ {
+ if (exception != null)
+ {
+ return exception;
+ }
+ else
+ {
+ return new AlfrescoRuntimeException(UNSPECIFIED_ERROR_MESSAGE);
+ }
+ }
+}
diff --git a/source/java/org/alfresco/util/PortUtil.java b/source/java/org/alfresco/util/PortUtil.java
new file mode 100644
index 0000000000..a9825e5975
--- /dev/null
+++ b/source/java/org/alfresco/util/PortUtil.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2005-2015 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.util;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Alfresco port-related utility functions.
+ */
+public class PortUtil
+{
+ private static Log logger = LogFactory.getLog(PortUtil.class);
+
+ /**
+ * Check if specified port is free.
+ * @param port port number to check
+ * @return true if port is free or false if it's already in use
+ */
+ public static boolean isPortFree(int port)
+ {
+ boolean isFree = true;
+ ServerSocket serverSocket = null;
+
+ try
+ {
+ serverSocket = new ServerSocket(port);
+ }
+ catch (IOException ioe)
+ {
+ isFree = false;
+ }
+ finally
+ {
+ if (serverSocket != null)
+ {
+ try
+ {
+ serverSocket.close();
+ }
+ catch (IOException ioe)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(ioe.toString());
+ }
+ }
+ }
+ }
+
+ return isFree;
+ }
+}