1 /*
2 * ModeShape (http://www.modeshape.org)
3 * See the COPYRIGHT.txt file distributed with this work for information
4 * regarding copyright ownership. Some portions may be licensed
5 * to Red Hat, Inc. under one or more contributor license agreements.
6 * See the AUTHORS.txt file in the distribution for a full listing of
7 * individual contributors.
8 *
9 * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10 * is licensed to you under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation; either version 2.1 of
12 * the License, or (at your option) any later version.
13 *
14 * ModeShape is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this software; if not, write to the Free
21 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23 */
24
25 package org.modeshape.common.component;
26
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.concurrent.CopyOnWriteArrayList;
31 import java.util.concurrent.atomic.AtomicReference;
32 import java.util.concurrent.locks.Lock;
33 import java.util.concurrent.locks.ReentrantLock;
34 import net.jcip.annotations.GuardedBy;
35 import net.jcip.annotations.ThreadSafe;
36 import org.modeshape.common.CommonI18n;
37 import org.modeshape.common.SystemFailureException;
38 import org.modeshape.common.util.CheckArg;
39 import org.modeshape.common.util.Reflection;
40
41 /**
42 * Maintains the list of component instances for the system. This class does not actively update the component configurations, but
43 * is designed to properly maintain the instances when those configurations are changed by other callers. If the components are
44 * subclasses of {@link Component}, then they will be {@link Component#setConfiguration(ComponentConfig) configured} with the
45 * appropriate configuration.
46 * <p>
47 * Therefore, this library does guarantee that the {@link #getInstances() instances} at the time they are {@link #getInstances()
48 * obtained} are always reflected by the configurations.
49 * </p>
50 *
51 * @param <ComponentType> the type of component being managed, which may be a subclass of {@link Component}
52 * @param <ConfigType> the configuration type describing the components
53 */
54 @ThreadSafe
55 public class ComponentLibrary<ComponentType, ConfigType extends ComponentConfig> {
56
57 /**
58 * Class loader factory instance that always returns the {@link Thread#getContextClassLoader() current thread's context class
59 * loader} (if not null) or component library's class loader.
60 */
61 public static final ClassLoaderFactory DEFAULT = new StandardClassLoaderFactory(ComponentLibrary.class.getClassLoader());
62
63 /**
64 * The class loader factory
65 */
66 private final AtomicReference<ClassLoaderFactory> classLoaderFactory = new AtomicReference<ClassLoaderFactory>(DEFAULT);
67
68 /**
69 * The list of component instances. The index of each component instance matches the corresponding configuration instance in
70 * {@link #configs}
71 */
72 @GuardedBy( value = "lock" )
73 private final List<ComponentType> instances = new CopyOnWriteArrayList<ComponentType>();
74 private final List<ConfigType> configs = new CopyOnWriteArrayList<ConfigType>();
75 private final List<ComponentType> unmodifiableInstances = Collections.unmodifiableList(instances);
76 private final Lock lock = new ReentrantLock();
77 private final boolean addBeforeExistingConfigs;
78
79 /**
80 * Create a new library of components.
81 */
82 public ComponentLibrary() {
83 this(false);
84 }
85
86 /**
87 * Create a new library of components.
88 *
89 * @param addBeforeExistingConfigs <code>true</code> if configurations should be {@link #add(ComponentConfig) added} before
90 * previously added configurations.
91 */
92 public ComponentLibrary( boolean addBeforeExistingConfigs ) {
93 this.addBeforeExistingConfigs = addBeforeExistingConfigs;
94 }
95
96 /**
97 * Get the class loader factory that should be used to load the component classes. Unless changed, the library uses the
98 * {@link #DEFAULT default} class loader factory, which uses the {@link Thread#getContextClassLoader() current thread's
99 * context class loader} if not null or the class loader that loaded the library class.
100 *
101 * @return the class loader factory; never null
102 * @see #setClassLoaderFactory(ClassLoaderFactory)
103 */
104 public ClassLoaderFactory getClassLoaderFactory() {
105 return this.classLoaderFactory.get();
106 }
107
108 /**
109 * Set the Maven Repository that should be used to load the component classes. Unless changed, the library uses the
110 * {@link #DEFAULT default} class loader factory, which uses the {@link Thread#getContextClassLoader() current thread's
111 * context class loader} if not null or the class loader that loaded the library class.
112 *
113 * @param classLoaderFactory the class loader factory reference, or null if the {@link #DEFAULT default class loader factory}
114 * should be used
115 * @see #getClassLoaderFactory()
116 */
117 public void setClassLoaderFactory( ClassLoaderFactory classLoaderFactory ) {
118 this.classLoaderFactory.set(classLoaderFactory != null ? classLoaderFactory : DEFAULT);
119 refreshInstances();
120 }
121
122 /**
123 * Add the configuration for a component, or update any existing one that represents the {@link ConfigType#equals(Object) same
124 * configuration}
125 *
126 * @param config the new configuration
127 * @return true if the component was added, or false if there already was an existing and
128 * {@link ComponentConfig#hasChanged(ComponentConfig) unchanged} component configuration
129 * @throws IllegalArgumentException if <code>config</code> is null
130 * @see #update(ComponentConfig)
131 * @see #remove(ComponentConfig)
132 */
133 public boolean add( ConfigType config ) {
134 CheckArg.isNotNull(config, "component configuration");
135 try {
136 this.lock.lock();
137 // Find an existing configuration that matches ...
138 int index = findIndexOfMatchingConfiguration(config);
139 if (index >= 0) {
140 // See if the matching configuration has changed ...
141 ConfigType existingConfig = this.configs.get(index);
142 if (existingConfig.hasChanged(config)) {
143 // It has changed, so we need to replace it ...
144 this.configs.set(index, config);
145 this.instances.set(index, newInstance(config));
146 }
147 return false;
148 }
149 // Didn't find one, so add it ...
150 if (addBeforeExistingConfigs) {
151 this.configs.add(0, config);
152 this.instances.add(0, newInstance(config));
153 } else {
154 this.configs.add(config);
155 this.instances.add(newInstance(config));
156 }
157 return true;
158 } finally {
159 this.lock.unlock();
160 }
161 }
162
163 /**
164 * Get the instance given by the configuration with the supplied name.
165 *
166 * @param name the configuration name
167 * @return the instance, or null if the configuration doesn't exist
168 */
169 public ComponentType getInstance( String name ) {
170 CheckArg.isNotNull(name, "name");
171 try {
172 this.lock.lock();
173 // Find an existing configuration that matches ...
174 int index = findIndexOfMatchingConfiguration(name);
175 return index >= 0 ? this.instances.get(index) : null;
176 } finally {
177 this.lock.unlock();
178 }
179 }
180
181 /**
182 * Update the configuration for a component, or add it if there is no {@link ConfigType#equals(Object) matching configuration}
183 * .
184 *
185 * @param config the updated (or new) configuration
186 * @return true if the component was updated, or false if there already was an existing and
187 * {@link ComponentConfig#hasChanged(ComponentConfig) unchanged} component configuration
188 * @throws IllegalArgumentException if <code>config</code> is null
189 * @see #add(ComponentConfig)
190 * @see #remove(ComponentConfig)
191 */
192 public boolean update( ConfigType config ) {
193 return add(config);
194 }
195
196 /**
197 * Remove the configuration for a component.
198 *
199 * @param config the configuration to be removed
200 * @return true if the component was remove, or false if there was no existing configuration
201 * @throws IllegalArgumentException if <code>config</code> is null
202 * @see #add(ComponentConfig)
203 * @see #update(ComponentConfig)
204 */
205 public boolean remove( ConfigType config ) {
206 CheckArg.isNotNull(config, "component configuration");
207 try {
208 this.lock.lock();
209 // Find an existing configuration that matches ...
210 int index = findIndexOfMatchingConfiguration(config);
211 if (index >= 0) {
212 // Remove the configuration and the component instance ...
213 this.configs.remove(index);
214 this.instances.remove(index);
215 return true;
216 }
217 return false;
218 } finally {
219 this.lock.unlock();
220 }
221 }
222
223 /**
224 * Refresh the instances by attempting to re-instantiate each registered configuration.
225 *
226 * @return true if at least one instance was instantiated, or false if none were
227 */
228 public boolean refreshInstances() {
229 try {
230 this.lock.lock();
231 // Loop through and create new instances for each configuration ...
232 boolean found = false;
233 int index = 0;
234 for (ConfigType config : this.configs) {
235 ComponentType instance = newInstance(config);
236 found = found || instance != null;
237 this.instances.set(index, instance);
238 ++index;
239 }
240 return found;
241 } finally {
242 this.lock.unlock();
243 }
244 }
245
246 /**
247 * Return the list of components.
248 *
249 * @return the unmodifiable list of components; never null
250 */
251 public List<ComponentType> getInstances() {
252 return this.unmodifiableInstances;
253 }
254
255 /**
256 * Instantiate, configure and return a new component described by the supplied configuration. This method does not manage the
257 * returned instance.
258 *
259 * @param config the configuration describing the component
260 * @return the new component, or null if the component could not be successfully configured
261 * @throws IllegalArgumentException if the component could not be configured properly
262 */
263 @SuppressWarnings( "unchecked" )
264 protected ComponentType newInstance( ConfigType config ) {
265 String[] classpath = config.getComponentClasspathArray();
266 final ClassLoader classLoader = this.getClassLoaderFactory().getClassLoader(classpath);
267 assert classLoader != null;
268 ComponentType newInstance = null;
269 try {
270 // Don't use ClassLoader.loadClass(String), as it doesn't properly initialize the class
271 // (specifically static initializers may not be called)
272 Class<?> componentClass = Class.forName(config.getComponentClassname(), true, classLoader);
273 newInstance = doCreateInstance(componentClass);
274 if (newInstance instanceof Component) {
275 ((Component<ConfigType>)newInstance).setConfiguration(config);
276 }
277
278 if (config.getProperties() != null) {
279 for (Map.Entry<String, Object> entry : config.getProperties().entrySet()) {
280 // Set the JavaBean-style property on the RepositorySource instance ...
281 Reflection reflection = new Reflection(newInstance.getClass());
282 reflection.invokeSetterMethodOnTarget(entry.getKey(), newInstance, entry.getValue());
283 }
284 }
285 } catch (Throwable e) {
286 throw new SystemFailureException(e);
287 }
288 if (newInstance instanceof Component && ((Component<ConfigType>)newInstance).getConfiguration() == null) {
289 throw new SystemFailureException(CommonI18n.componentNotConfigured.text(config.getName()));
290 }
291 return newInstance;
292 }
293
294 /**
295 * Method that instantiates the supplied class. This method can be overridden by subclasses that may need to wrap or adapt the
296 * instance to be a ComponentType.
297 *
298 * @param componentClass
299 * @return the new ComponentType instance
300 * @throws InstantiationException
301 * @throws IllegalAccessException
302 */
303 @SuppressWarnings( "unchecked" )
304 protected ComponentType doCreateInstance( Class<?> componentClass ) throws InstantiationException, IllegalAccessException {
305 return (ComponentType)componentClass.newInstance();
306 }
307
308 /**
309 * Find the index for the matching {@link #configs configuration} and {@link #instances component}.
310 *
311 * @param config the configuration; may not be null
312 * @return the index, or -1 if not found
313 */
314 @GuardedBy( value = "lock" )
315 protected int findIndexOfMatchingConfiguration( ConfigType config ) {
316 // Iterate through the configurations and look for an existing one that matches
317 for (int i = 0, length = this.configs.size(); i != length; i++) {
318 ConfigType existingConfig = this.configs.get(i);
319 assert existingConfig != null;
320 if (existingConfig.equals(config)) {
321 return i;
322 }
323 }
324 return -1;
325 }
326
327 /**
328 * Find the index for the matching {@link #configs configuration} and {@link #instances component}.
329 *
330 * @param name the configuration name; may not be null
331 * @return the index, or -1 if not found
332 */
333 @GuardedBy( value = "lock" )
334 protected int findIndexOfMatchingConfiguration( String name ) {
335 // Iterate through the configurations and look for an existing one that matches
336 for (int i = 0, length = this.configs.size(); i != length; i++) {
337 ConfigType existingConfig = this.configs.get(i);
338 assert existingConfig != null;
339 if (existingConfig.getName().equals(name)) {
340 return i;
341 }
342 }
343 return -1;
344 }
345 }