View Javadoc

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  package org.modeshape.repository;
25  
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.util.Map;
29  import java.util.concurrent.TimeUnit;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  import net.jcip.annotations.ThreadSafe;
32  import org.modeshape.common.collection.Problems;
33  import org.modeshape.common.collection.SimpleProblems;
34  import org.modeshape.common.util.CheckArg;
35  import org.modeshape.common.util.Logger;
36  import org.modeshape.common.util.Reflection;
37  import org.modeshape.graph.ExecutionContext;
38  import org.modeshape.graph.Graph;
39  import org.modeshape.graph.JcrLexicon;
40  import org.modeshape.graph.Location;
41  import org.modeshape.graph.Node;
42  import org.modeshape.graph.Subgraph;
43  import org.modeshape.graph.connector.RepositorySource;
44  import org.modeshape.graph.observe.Changes;
45  import org.modeshape.graph.observe.NetChangeObserver;
46  import org.modeshape.graph.observe.ObservationBus;
47  import org.modeshape.graph.observe.Observer;
48  import org.modeshape.graph.property.Name;
49  import org.modeshape.graph.property.Path;
50  import org.modeshape.graph.property.PathFactory;
51  import org.modeshape.graph.property.PathNotFoundException;
52  import org.modeshape.graph.property.Property;
53  import org.modeshape.graph.property.PropertyType;
54  import org.modeshape.graph.property.ValueFactories;
55  import org.modeshape.graph.property.ValueFactory;
56  import org.modeshape.graph.request.ReadBranchRequest;
57  import org.modeshape.repository.service.AbstractServiceAdministrator;
58  import org.modeshape.repository.service.AdministeredService;
59  import org.modeshape.repository.service.ServiceAdministrator;
60  
61  /**
62   * A service that manages the {@link RepositorySource}es defined within a configuration repository.
63   */
64  @ThreadSafe
65  public class RepositoryService implements AdministeredService, Observer {
66  
67      /**
68       * The administrative component for this service.
69       * 
70       * @author Randall Hauch
71       */
72      protected class Administrator extends AbstractServiceAdministrator {
73  
74          protected Administrator() {
75              super(RepositoryI18n.repositoryServiceName, State.PAUSED);
76          }
77  
78          /**
79           * {@inheritDoc}
80           */
81          @Override
82          protected boolean doCheckIsTerminated() {
83              return true;
84          }
85  
86          /**
87           * {@inheritDoc}
88           */
89          @Override
90          protected void doStart( State fromState ) {
91              super.doStart(fromState);
92              startService();
93          }
94  
95          /**
96           * {@inheritDoc}
97           * 
98           * @see org.modeshape.repository.service.AbstractServiceAdministrator#doShutdown(org.modeshape.repository.service.ServiceAdministrator.State)
99           */
100         @Override
101         protected void doShutdown( State fromState ) {
102             super.doShutdown(fromState);
103             shutdownService();
104         }
105 
106         /**
107          * {@inheritDoc}
108          * 
109          * @see org.modeshape.repository.service.ServiceAdministrator#awaitTermination(long, java.util.concurrent.TimeUnit)
110          */
111         public boolean awaitTermination( long timeout,
112                                          TimeUnit unit ) {
113             return true;
114         }
115     }
116 
117     private final ExecutionContext context;
118     private final RepositoryLibrary sources;
119     private final String configurationSourceName;
120     private final String configurationWorkspaceName;
121     private final Path pathToConfigurationRoot;
122     private final ConfigurationChangeObserver configurationChangeObserver;
123     private final Administrator administrator = new Administrator();
124     private final AtomicBoolean started = new AtomicBoolean(false);
125     /** The problem sink used when encountering problems while starting repositories */
126     private final Problems problems;
127 
128     /**
129      * Create a service instance, reading the configuration describing new {@link RepositorySource} instances from the supplied
130      * configuration repository.
131      * 
132      * @param configurationSource the {@link RepositorySource} that is the configuration repository
133      * @param configurationWorkspaceName the name of the workspace in the {@link RepositorySource} that is the configuration
134      *        repository, or null if the default workspace of the source should be used (if there is one)
135      * @param pathToConfigurationRoot the path of the node in the configuration source repository that should be treated by this
136      *        service as the root of the service's configuration; if null, then "/dna:system" is used
137      * @param context the execution context in which this service should run
138      * @param observationBus the {@link ObservationBus} instance that should be used for changes in the sources
139      * @param problems the {@link Problems} instance that this service should use to report problems starting repositories
140      * @throws IllegalArgumentException if the bootstrap source is null or the execution context is null
141      */
142     public RepositoryService( RepositorySource configurationSource,
143                               String configurationWorkspaceName,
144                               Path pathToConfigurationRoot,
145                               ExecutionContext context,
146                               ObservationBus observationBus,
147                               Problems problems ) {
148         CheckArg.isNotNull(configurationSource, "configurationSource");
149         CheckArg.isNotNull(context, "context");
150         CheckArg.isNotNull(observationBus, "observationBus");
151         PathFactory pathFactory = context.getValueFactories().getPathFactory();
152         if (pathToConfigurationRoot == null) pathToConfigurationRoot = pathFactory.create("/dna:system");
153         if (problems == null) problems = new SimpleProblems();
154         Path sourcesPath = pathFactory.create(pathToConfigurationRoot, ModeShapeLexicon.SOURCES);
155 
156         this.sources = new RepositoryLibrary(configurationSource, configurationWorkspaceName, sourcesPath, context,
157                                              observationBus);
158         this.sources.addSource(configurationSource);
159         this.pathToConfigurationRoot = pathToConfigurationRoot;
160         this.configurationSourceName = configurationSource.getName();
161         this.configurationWorkspaceName = configurationWorkspaceName;
162         this.context = context;
163         this.problems = problems;
164         this.configurationChangeObserver = new ConfigurationChangeObserver();
165     }
166 
167     /**
168      * {@inheritDoc}
169      */
170     public final ServiceAdministrator getAdministrator() {
171         return this.administrator;
172     }
173 
174     /**
175      * @return configurationSourceName
176      */
177     public final String getConfigurationSourceName() {
178         return configurationSourceName;
179     }
180 
181     /**
182      * @return configurationWorkspaceName
183      */
184     public final String getConfigurationWorkspaceName() {
185         return configurationWorkspaceName;
186     }
187 
188     /**
189      * Get the library of {@link RepositorySource} instances used by this service.
190      * 
191      * @return the RepositorySource library; never null
192      */
193     public final RepositoryLibrary getRepositoryLibrary() {
194         return sources;
195     }
196 
197     /**
198      * @return pathToConfigurationRoot
199      */
200     protected final Path getPathToConfigurationRoot() {
201         return pathToConfigurationRoot;
202     }
203 
204     /**
205      * @return env
206      */
207     public final ExecutionContext getExecutionEnvironment() {
208         return context;
209     }
210 
211     public String getJndiName() {
212         // TODO
213         return null;
214     }
215 
216     protected synchronized void startService() {
217         if (this.started.get() == false) {
218             // ------------------------------------------------------------------------------------
219             // Read the configuration ...
220             // ------------------------------------------------------------------------------------
221 
222             // Read the configuration and repository source nodes (children under "/dna:sources") ...
223             Graph graph = Graph.create(getConfigurationSourceName(), sources, context);
224             Path pathToSourcesNode = context.getValueFactories().getPathFactory().create(pathToConfigurationRoot,
225                                                                                          ModeShapeLexicon.SOURCES);
226             try {
227                 String workspaceName = getConfigurationWorkspaceName();
228                 if (workspaceName != null) graph.useWorkspace(workspaceName);
229 
230                 Subgraph sourcesGraph = graph.getSubgraphOfDepth(ReadBranchRequest.NO_MAXIMUM_DEPTH).at(pathToSourcesNode);
231 
232                 // Iterate over each of the children, and create the RepositorySource ...
233                 for (Location location : sourcesGraph.getRoot().getChildren()) {
234                     sources.addSource(createRepositorySource(sourcesGraph, location, problems));
235                 }
236             } catch (PathNotFoundException e) {
237                 // No sources were found, and this is okay!
238             } catch (Throwable err) {
239                 throw new ModeShapeConfigurationException(RepositoryI18n.errorStartingRepositoryService.text(), err);
240             }
241 
242             this.started.set(true);
243         }
244     }
245 
246     protected synchronized void shutdownService() {
247         // Close the repository library ...
248         this.sources.getAdministrator().shutdown();
249     }
250 
251     /**
252      * Instantiate the {@link RepositorySource} described by the supplied properties.
253      * 
254      * @param subgraph the subgraph containing the configuration information for this {@link RepositorySource}
255      * @param location the location of the properties to apply to the new {@link RepositorySource}
256      * @param problems the problems container in which any problems should be reported; never null
257      * @return the repository source instance, or null if it could not be created
258      */
259     protected RepositorySource createRepositorySource( Subgraph subgraph,
260                                                        Location location,
261                                                        Problems problems ) {
262         return (RepositorySource)createInstanceFromProperties(subgraph, location, problems, true);
263     }
264 
265     /**
266      * Instantiate the {@link Object} described by the supplied properties.
267      * 
268      * @param subgraph the subgraph containing the configuration information for this instance
269      * @param location the location of the properties to apply to the new instance
270      * @param problems the problems container in which any problems should be reported; never null
271      * @param mustHaveClassName indicates that the properties must include a class name; if true a problem will be added for
272      *        instances that do not have a class name specified
273      * @return the instance, or null if it could not be created
274      */
275     protected Object createInstanceFromProperties( Subgraph subgraph,
276                                                    Location location,
277                                                    Problems problems,
278                                                    boolean mustHaveClassName ) {
279         ValueFactories valueFactories = context.getValueFactories();
280         ValueFactory<String> stringFactory = valueFactories.getStringFactory();
281 
282         Node node = subgraph.getNode(location);
283         assert location.hasPath();
284         Path path = node.getLocation().getPath();
285         Map<Name, Property> properties = node.getPropertiesByName();
286 
287         // Get the classname and classpath ...
288         Property classnameProperty = properties.get(ModeShapeLexicon.CLASSNAME);
289         Property classpathProperty = properties.get(ModeShapeLexicon.CLASSPATH);
290         if (classnameProperty == null) {
291             if (mustHaveClassName) {
292                 problems.addError(RepositoryI18n.requiredPropertyIsMissingFromNode, ModeShapeLexicon.CLASSNAME, path);
293             }
294             return null;
295         }
296         // If the classpath property is null or empty, the default classpath will be used
297         if (problems.hasErrors()) return null;
298 
299         // Create the instance ...
300         String classname = stringFactory.create(classnameProperty.getValues().next());
301         String[] classpath = classpathProperty == null ? new String[] {} : stringFactory.create(classpathProperty.getValuesAsArray());
302         ClassLoader classLoader = context.getClassLoader(classpath);
303         Object instance = null;
304         try {
305             Class<?> sourceClass = classLoader.loadClass(classname);
306             instance = sourceClass.newInstance();
307         } catch (ClassNotFoundException err) {
308             problems.addError(err, RepositoryI18n.unableToLoadClassUsingClasspath, classname, classpath);
309         } catch (IllegalAccessException err) {
310             problems.addError(err, RepositoryI18n.unableToAccessClassUsingClasspath, classname, classpath);
311         } catch (Throwable err) {
312             problems.addError(err, RepositoryI18n.unableToInstantiateClassUsingClasspath, classname, classpath);
313         }
314         if (instance == null) return null;
315 
316         // We need to set the name using the local name of the node...
317         Property nameProperty = context.getPropertyFactory().create(JcrLexicon.NAME,
318                                                                     path.getLastSegment().getName().getLocalName());
319         properties.put(JcrLexicon.NAME, nameProperty);
320 
321         // Attempt to set the configuration information as bean properties,
322         // if they exist on the object and are not already set to some value ...
323         setBeanPropertyIfExistsAndNotSet(instance, "configurationSourceName", getConfigurationSourceName());
324         setBeanPropertyIfExistsAndNotSet(instance, "configurationWorkspaceName", getConfigurationWorkspaceName());
325         setBeanPropertyIfExistsAndNotSet(instance, "configurationPath", stringFactory.create(path));
326 
327         // Now set all the properties that we can, ignoring any property that doesn't fit the pattern ...
328         Reflection reflection = new Reflection(instance.getClass());
329         for (Map.Entry<Name, Property> entry : properties.entrySet()) {
330             Name propertyName = entry.getKey();
331             Property property = entry.getValue();
332             String javaPropertyName = propertyName.getLocalName();
333             if (property.isEmpty()) continue;
334 
335             Object value = null;
336             Method setter = null;
337             try {
338                 setter = reflection.findFirstMethod("set" + javaPropertyName, false);
339                 if (setter == null) continue;
340                 // Determine the type of the one parameter ...
341                 Class<?>[] parameterTypes = setter.getParameterTypes();
342                 if (parameterTypes.length != 1) continue; // not a valid JavaBean property
343                 Class<?> paramType = parameterTypes[0];
344                 PropertyType allowedType = PropertyType.discoverType(paramType);
345                 if (allowedType == null) continue; // assume not a JavaBean property with usable type
346                 ValueFactory<?> factory = context.getValueFactories().getValueFactory(allowedType);
347                 if (paramType.isArray()) {
348                     if (paramType.getComponentType().isArray()) continue; // array of array, which we don't do
349                     Object[] values = factory.create(property.getValuesAsArray());
350                     // Convert to an array of primitives if that's what the signature requires ...
351                     Class<?> componentType = paramType.getComponentType();
352                     if (Integer.TYPE.equals(componentType)) {
353                         int[] primitiveValues = new int[values.length];
354                         for (int i = 0; i != values.length; ++i) {
355                             primitiveValues[i] = ((Long)values[i]).intValue();
356                         }
357                         value = primitiveValues;
358                     } else if (Short.TYPE.equals(componentType)) {
359                         short[] primitiveValues = new short[values.length];
360                         for (int i = 0; i != values.length; ++i) {
361                             primitiveValues[i] = ((Long)values[i]).shortValue();
362                         }
363                         value = primitiveValues;
364                     } else if (Long.TYPE.equals(componentType)) {
365                         long[] primitiveValues = new long[values.length];
366                         for (int i = 0; i != values.length; ++i) {
367                             primitiveValues[i] = ((Long)values[i]).longValue();
368                         }
369                         value = primitiveValues;
370                     } else if (Double.TYPE.equals(componentType)) {
371                         double[] primitiveValues = new double[values.length];
372                         for (int i = 0; i != values.length; ++i) {
373                             primitiveValues[i] = ((Double)values[i]).doubleValue();
374                         }
375                         value = primitiveValues;
376                     } else if (Float.TYPE.equals(componentType)) {
377                         float[] primitiveValues = new float[values.length];
378                         for (int i = 0; i != values.length; ++i) {
379                             primitiveValues[i] = ((Double)values[i]).floatValue();
380                         }
381                         value = primitiveValues;
382                     } else if (Boolean.TYPE.equals(componentType)) {
383                         boolean[] primitiveValues = new boolean[values.length];
384                         for (int i = 0; i != values.length; ++i) {
385                             primitiveValues[i] = ((Boolean)values[i]).booleanValue();
386                         }
387                         value = primitiveValues;
388                     } else {
389                         value = values;
390                     }
391                 } else {
392                     value = factory.create(property.getFirstValue());
393                     // Convert to the correct primitive, if needed ...
394                     if (Integer.TYPE.equals(paramType)) {
395                         value = new Integer(((Long)value).intValue());
396                     } else if (Short.TYPE.equals(paramType)) {
397                         value = new Short(((Long)value).shortValue());
398                     } else if (Float.TYPE.equals(paramType)) {
399                         value = new Float(((Double)value).floatValue());
400                     }
401                 }
402                 // Invoke the method ...
403                 String msg = "Setting property {0} to {1} on source at {2} in configuration repository {3} in workspace {4}";
404                 Logger.getLogger(getClass()).trace(msg,
405                                                    javaPropertyName,
406                                                    value,
407                                                    path,
408                                                    configurationSourceName,
409                                                    configurationWorkspaceName);
410                 setter.invoke(instance, value);
411             } catch (SecurityException err) {
412                 Logger.getLogger(getClass()).debug(err, "Error invoking {0}.{1}", instance.getClass(), setter);
413             } catch (IllegalArgumentException err) {
414                 // Do nothing ... assume not a JavaBean property (but log)
415                 String msg = "Invalid argument invoking {0} with parameter {1} on source at {2} in configuration repository {3} in workspace {4}";
416                 Logger.getLogger(getClass()).debug(err,
417                                                    msg,
418                                                    setter,
419                                                    value,
420                                                    path,
421                                                    configurationSourceName,
422                                                    configurationWorkspaceName);
423             } catch (IllegalAccessException err) {
424                 Logger.getLogger(getClass()).debug(err, "Error invoking {0}.{1}", instance.getClass(), setter);
425             } catch (InvocationTargetException err) {
426                 // Do nothing ... assume not a JavaBean property (but log)
427                 String msg = "Error invoking {0} with parameter {1} on source at {2} in configuration repository {3} in workspace {4}";
428                 Logger.getLogger(getClass()).debug(err.getTargetException(),
429                                                    msg,
430                                                    setter,
431                                                    value,
432                                                    path,
433                                                    configurationSourceName,
434                                                    configurationWorkspaceName);
435             }
436         }
437 
438         // Check for nested instances in the configuration
439         for (Location childLocation : node.getChildren()) {
440             assert childLocation.hasPath();
441             Path childPath = childLocation.getPath();
442             Name childName = childPath.getLastSegment().getName();
443 
444             Object value = createInstanceFromProperties(subgraph, childLocation, problems, false);
445             if (problems.hasErrors()) {
446                 return null;
447             }
448 
449             String javaPropertyName = childName.getLocalName();
450             Method setter = reflection.findFirstMethod("set" + javaPropertyName, false);
451             if (setter == null) continue;
452 
453             try {
454                 setter.invoke(instance, value);
455                 // Invoke the method ...
456                 String msg = "Setting property {0} to {1} on object at {2} in configuration repository {3} in workspace {4}";
457                 Logger.getLogger(getClass()).trace(msg,
458                                                    javaPropertyName,
459                                                    value,
460                                                    childPath,
461                                                    configurationSourceName,
462                                                    configurationWorkspaceName);
463                 setter.invoke(instance, value);
464             } catch (SecurityException err) {
465                 Logger.getLogger(getClass()).debug(err, "Error invoking {0}.{1}", instance.getClass(), setter);
466             } catch (IllegalArgumentException err) {
467                 // Do nothing ... assume not a JavaBean property (but log)
468                 String msg = "Invalid argument invoking {0} with parameter {1} on object at {2} in configuration repository {3} in workspace {4}";
469                 Logger.getLogger(getClass()).debug(err,
470                                                    msg,
471                                                    setter,
472                                                    value,
473                                                    childPath,
474                                                    configurationSourceName,
475                                                    configurationWorkspaceName);
476             } catch (IllegalAccessException err) {
477                 Logger.getLogger(getClass()).debug(err, "Error invoking {0}.{1}", instance.getClass(), setter);
478             } catch (InvocationTargetException err) {
479                 // Do nothing ... assume not a JavaBean property (but log)
480                 String msg = "Error invoking {0} with parameter {1} on source at {2} in configuration repository {3} in workspace {4}";
481                 Logger.getLogger(getClass()).debug(err.getTargetException(),
482                                                    msg,
483                                                    setter,
484                                                    value,
485                                                    childPath,
486                                                    configurationSourceName,
487                                                    configurationWorkspaceName);
488             }
489 
490         }
491 
492         return instance;
493 
494     }
495 
496     protected boolean setBeanPropertyIfExistsAndNotSet( Object target,
497                                                         String propertyName,
498                                                         Object value ) {
499         Reflection reflection = new Reflection(target.getClass());
500         try {
501             if (reflection.invokeGetterMethodOnTarget(propertyName, target) == null) {
502                 reflection.invokeSetterMethodOnTarget(propertyName, target, value);
503                 return true;
504             }
505             return false;
506         } catch (Exception e) {
507             // Log that the property was not found ...
508             Logger.getLogger(getClass())
509                   .debug("Unknown property '{0}' on '{1}' class", propertyName, target.getClass().getName());
510             return false;
511         }
512     }
513 
514     /**
515      * {@inheritDoc}
516      */
517     @Override
518     public boolean equals( Object obj ) {
519         if (obj == this) return true;
520         return false;
521     }
522 
523     /**
524      * {@inheritDoc}
525      * 
526      * @see org.modeshape.graph.observe.Observer#notify(org.modeshape.graph.observe.Changes)
527      */
528     public void notify( Changes changes ) {
529         // Forward the changes to the net change observer ...
530         this.configurationChangeObserver.notify(changes);
531     }
532 
533     protected class ConfigurationChangeObserver extends NetChangeObserver {
534 
535         /**
536          * {@inheritDoc}
537          * 
538          * @see org.modeshape.graph.observe.NetChangeObserver#notify(org.modeshape.graph.observe.NetChangeObserver.NetChanges)
539          */
540         @Override
541         protected void notify( NetChanges netChanges ) {
542             if (getConfigurationWorkspaceName() == null) {
543                 // This was a transient configuration source, so it should never change ...
544                 return;
545             }
546             if (!getConfigurationSourceName().equals(netChanges.getSourceName())) return;
547             for (NetChange change : netChanges.getNetChanges()) {
548                 if (!getConfigurationWorkspaceName().equals(change.getRepositoryWorkspaceName())) return;
549                 Path changedPath = change.getPath();
550                 Path configPath = getPathToConfigurationRoot();
551                 if (!changedPath.isAtOrBelow(getPathToConfigurationRoot())) return;
552                 boolean changedNodeIsPotentiallySource = configPath.size() + 1 == changedPath.size();
553 
554                 // At this point, we know that something inside the configuration changed, so figure out what happened ...
555                 if (changedNodeIsPotentiallySource && change.includes(ChangeType.NODE_REMOVED)) {
556                     // Then potentially a source with the supplied name has been removed ...
557                     String sourceName = changedPath.getLastSegment().getName().getLocalName();
558                     getRepositoryLibrary().removeSource(sourceName);
559                 } else {
560                     // The add/change/remove is either at or below a source, so try to create a new source for it ...
561                     Path sourcePath = changedNodeIsPotentiallySource ? changedPath : changedPath.subpath(0, configPath.size() + 1);
562                     Problems problems = new SimpleProblems();
563                     // Now read the node and create the source ...
564                     Graph graph = Graph.create(getConfigurationSourceName(), getRepositoryLibrary(), getExecutionEnvironment());
565                     try {
566                         String workspaceName = getConfigurationWorkspaceName();
567                         if (workspaceName != null) graph.useWorkspace(workspaceName);
568                         Subgraph subgraph = graph.getSubgraphOfDepth(ReadBranchRequest.NO_MAXIMUM_DEPTH).at(sourcePath);
569                         RepositorySource source = createRepositorySource(subgraph, Location.create(sourcePath), problems);
570                         if (source != null) {
571                             // It was the config for a source, so try to add or replace an existing source ...
572                             getRepositoryLibrary().addSource(source, true);
573                         }
574                     } catch (PathNotFoundException e) {
575                         // No source was found, and this is okay (since it may just been deleted)...
576                         String sourceName = changedPath.getLastSegment().getName().getLocalName();
577                         getRepositoryLibrary().removeSource(sourceName);
578                     }
579                 }
580             }
581         }
582     }
583 }