001 /*
002 * JBoss, Home of Professional Open Source.
003 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004 * as indicated by the @author tags. See the copyright.txt file in the
005 * distribution for a full listing of individual contributors.
006 *
007 * This is free software; you can redistribute it and/or modify it
008 * under the terms of the GNU Lesser General Public License as
009 * published by the Free Software Foundation; either version 2.1 of
010 * the License, or (at your option) any later version.
011 *
012 * This software is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this software; if not, write to the Free
019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021 */
022 package org.jboss.dna.repository;
023
024 import java.util.ArrayList;
025 import java.util.HashMap;
026 import java.util.HashSet;
027 import java.util.List;
028 import java.util.Map;
029 import java.util.Set;
030 import java.util.concurrent.ExecutorService;
031 import java.util.concurrent.ScheduledThreadPoolExecutor;
032 import java.util.concurrent.TimeUnit;
033 import net.jcip.annotations.Immutable;
034 import org.jboss.dna.common.collection.Problem;
035 import org.jboss.dna.common.collection.Problems;
036 import org.jboss.dna.common.collection.SimpleProblems;
037 import org.jboss.dna.common.util.CheckArg;
038 import org.jboss.dna.common.util.Logger;
039 import org.jboss.dna.graph.ExecutionContext;
040 import org.jboss.dna.graph.Graph;
041 import org.jboss.dna.graph.JcrLexicon;
042 import org.jboss.dna.graph.JcrMixLexicon;
043 import org.jboss.dna.graph.JcrNtLexicon;
044 import org.jboss.dna.graph.Location;
045 import org.jboss.dna.graph.Node;
046 import org.jboss.dna.graph.Subgraph;
047 import org.jboss.dna.graph.connector.RepositoryConnection;
048 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
049 import org.jboss.dna.graph.connector.RepositorySource;
050 import org.jboss.dna.graph.connector.RepositorySourceException;
051 import org.jboss.dna.graph.mimetype.ExtensionBasedMimeTypeDetector;
052 import org.jboss.dna.graph.mimetype.MimeTypeDetector;
053 import org.jboss.dna.graph.mimetype.MimeTypeDetectorConfig;
054 import org.jboss.dna.graph.mimetype.MimeTypeDetectors;
055 import org.jboss.dna.graph.property.Name;
056 import org.jboss.dna.graph.property.Path;
057 import org.jboss.dna.graph.property.PathExpression;
058 import org.jboss.dna.graph.property.PathNotFoundException;
059 import org.jboss.dna.graph.property.Property;
060 import org.jboss.dna.repository.sequencer.SequencerConfig;
061 import org.jboss.dna.repository.sequencer.SequencingService;
062
063 /**
064 * A single instance of the DNA services, which is obtained after setting up the {@link DnaConfiguration#build() configuration}.
065 *
066 * @see DnaConfiguration
067 */
068 @Immutable
069 public class DnaEngine {
070
071 public static final String CONFIGURATION_REPOSITORY_NAME = "dna:configuration";
072
073 protected final DnaConfiguration.ConfigurationDefinition configuration;
074 private final ConfigurationScanner scanner;
075 private final Problems problems;
076 protected final ExecutionContext context;
077
078 private final RepositoryService repositoryService;
079 private final SequencingService sequencingService;
080 private final ExecutorService executorService;
081 private final MimeTypeDetectors detectors;
082
083 private final RepositoryConnectionFactory connectionFactory;
084
085 protected DnaEngine( ExecutionContext context,
086 DnaConfiguration.ConfigurationDefinition configuration ) {
087 this.problems = new SimpleProblems();
088
089 // Use the configuration's context ...
090 this.detectors = new MimeTypeDetectors();
091 this.context = context.with(detectors);
092
093 // And set up the scanner ...
094 this.configuration = configuration;
095 this.scanner = new ConfigurationScanner(this.problems, this.context, this.configuration);
096
097 // Add the mime type detectors in the configuration ...
098 for (MimeTypeDetectorConfig config : scanner.getMimeTypeDetectors()) {
099 detectors.addDetector(config);
100 }
101 // Add an extension-based detector by default ...
102 detectors.addDetector(new MimeTypeDetectorConfig("ExtensionDetector", "Extension-based MIME type detector",
103 ExtensionBasedMimeTypeDetector.class));
104
105 // Create the RepositoryService, pointing it to the configuration repository ...
106 Path pathToConfigurationRoot = this.configuration.getPath();
107 String configWorkspaceName = this.configuration.getWorkspace();
108 final RepositorySource configSource = this.configuration.getRepositorySource();
109 repositoryService = new RepositoryService(configSource, configWorkspaceName, pathToConfigurationRoot, context);
110
111 // Create the sequencing service ...
112 executorService = new ScheduledThreadPoolExecutor(10); // Use a magic number for now
113 sequencingService = new SequencingService();
114 sequencingService.setExecutionContext(context);
115 sequencingService.setExecutorService(executorService);
116 sequencingService.setRepositoryLibrary(repositoryService.getRepositoryLibrary());
117 for (SequencerConfig sequencerConfig : scanner.getSequencingConfigurations()) {
118 sequencingService.addSequencer(sequencerConfig);
119 }
120
121 // Set up the connection factory for this engine ...
122 connectionFactory = new RepositoryConnectionFactory() {
123 public RepositoryConnection createConnection( String sourceName ) throws RepositorySourceException {
124 RepositorySource source = DnaEngine.this.getRepositorySource(sourceName);
125 if (source == null) {
126 throw new RepositorySourceException(sourceName);
127 }
128
129 return source.getConnection();
130 }
131 };
132 }
133
134 /**
135 * Get the problems that were encountered when setting up this engine from the configuration.
136 *
137 * @return the problems, which may be empty but will never be null
138 */
139 public Problems getProblems() {
140 return problems;
141 }
142
143 /**
144 * Get the context in which this engine is executing.
145 *
146 * @return the execution context; never null
147 */
148 public final ExecutionContext getExecutionContext() {
149 return context;
150 }
151
152 /**
153 * Get the {@link RepositorySource} instance used by this engine.
154 *
155 * @param repositoryName the name of the repository source
156 * @return the source, or null if no source with the given name exists
157 * @throws IllegalStateException if this engine was not {@link #start() started}
158 */
159 public final RepositorySource getRepositorySource( String repositoryName ) {
160 checkRunning();
161 return repositoryService.getRepositoryLibrary().getSource(repositoryName);
162 }
163
164 /**
165 * Get a factory of connections, backed by the RepositorySor
166 *
167 * @return the connection factory; never null
168 * @throws IllegalStateException if this engine was not {@link #start() started}
169 */
170 public final RepositoryConnectionFactory getRepositoryConnectionFactory() {
171 checkRunning();
172 return connectionFactory;
173 }
174
175 /**
176 * Get the repository service.
177 *
178 * @return the repository service owned by this engine; never null
179 * @throws IllegalStateException if this engine was not {@link #start() started}
180 */
181 public final RepositoryService getRepositoryService() {
182 checkRunning();
183 return repositoryService;
184 }
185
186 /**
187 * Get a graph to the underlying source.
188 *
189 * @param sourceName the name of the source
190 * @return the graph
191 * @throws IllegalArgumentException if the source name is null
192 * @throws RepositorySourceException if a source with the supplied name does not exist
193 * @throws IllegalStateException if this engine was not {@link #start() started}
194 */
195 public final Graph getGraph( String sourceName ) {
196 CheckArg.isNotNull(sourceName, "sourceName");
197 return getGraph(getExecutionContext(), sourceName);
198 }
199
200 /**
201 * Get a graph to the underlying source, using the supplied context. Note that the supplied context should be a derivative of
202 * the engine's {@link #getExecutionContext() context}.
203 *
204 * @param context the context of execution for this graph; may not be null
205 * @param sourceName the name of the source
206 * @return the graph
207 * @throws IllegalArgumentException if the context or source name are null
208 * @throws RepositorySourceException if a source with the supplied name does not exist
209 * @throws IllegalStateException if this engine was not {@link #start() started}
210 */
211 public final Graph getGraph( ExecutionContext context,
212 String sourceName ) {
213 CheckArg.isNotNull(context, "context");
214 CheckArg.isNotNull(sourceName, "sourceName");
215 checkRunning();
216 Graph graph = Graph.create(sourceName, getRepositoryService().getRepositoryLibrary(), context);
217 if (configuration.getRepositorySource().getName().equals(sourceName) && configuration.getWorkspace() != null) {
218 // set the workspace ...
219 graph.useWorkspace(configuration.getWorkspace());
220 }
221 return graph;
222 }
223
224 /**
225 * Get the sequencing service.
226 *
227 * @return the sequencing service owned by this engine; never null
228 * @throws IllegalStateException if this engine was not {@link #start() started}
229 */
230 public final SequencingService getSequencingService() {
231 checkRunning();
232 return sequencingService;
233 }
234
235 /**
236 * Return the component that is able to detect MIME types given the name of a stream and a stream.
237 *
238 * @return the MIME type detector used by this engine; never null
239 * @throws IllegalStateException if this engine was not {@link #start() started}
240 */
241 protected final MimeTypeDetector getMimeTypeDetector() {
242 checkRunning();
243 return detectors;
244 }
245
246 protected final boolean checkRunning() {
247 if (repositoryService.getAdministrator().isStarted() && sequencingService.getAdministrator().isStarted()) {
248 return true;
249 }
250 throw new IllegalStateException(RepositoryI18n.engineIsNotRunning.text());
251 }
252
253 /*
254 * Lifecycle methods
255 */
256 /**
257 * Start this engine to make it available for use.
258 *
259 * @throws IllegalStateException if this method is called when already shut down.
260 * @see #shutdown()
261 */
262 public void start() {
263 if (getProblems().hasErrors()) {
264 // First log the messages ...
265 Logger log = Logger.getLogger(getClass());
266 log.error(RepositoryI18n.errorsPreventStarting);
267 for (Problem problem : getProblems()) {
268 log.error(problem.getMessage(), problem.getParameters());
269 }
270 // Then throw an exception ...
271 throw new IllegalStateException(RepositoryI18n.errorsPreventStarting.text());
272 }
273 repositoryService.getAdministrator().start();
274 sequencingService.getAdministrator().start();
275 }
276
277 /**
278 * Shutdown this engine to close all connections, terminate any ongoing background operations (such as sequencing), and
279 * reclaim any resources that were acquired by this engine. This method may be called multiple times, but only the first time
280 * has an effect.
281 *
282 * @see #start()
283 */
284 public void shutdown() {
285 // First, shutdown the sequencing service, which will prevent any additional jobs from going through ...
286 sequencingService.getAdministrator().shutdown();
287
288 // Then terminate the executor service, which may be running background jobs that are not yet completed ...
289 try {
290 executorService.awaitTermination(10 * 60, TimeUnit.SECONDS); // No TimeUnit.MINUTES in JDK 5
291 } catch (InterruptedException ie) {
292 // Reset the thread's status and continue this method ...
293 Thread.interrupted();
294 }
295 executorService.shutdown();
296
297 // Finally shut down the repository source, which closes all connections ...
298 repositoryService.getAdministrator().shutdown();
299 }
300
301 /**
302 * Blocks until the shutdown has completed, or the timeout occurs, or the current thread is interrupted, whichever happens
303 * first.
304 *
305 * @param timeout the maximum time to wait for each component in this engine
306 * @param unit the time unit of the timeout argument
307 * @return <tt>true</tt> if this service complete shut down and <tt>false</tt> if the timeout elapsed before it was shut down
308 * completely
309 * @throws InterruptedException if interrupted while waiting
310 */
311 public boolean awaitTermination( long timeout,
312 TimeUnit unit ) throws InterruptedException {
313 if (!sequencingService.getAdministrator().awaitTermination(timeout, unit)) return false;
314 if (!executorService.awaitTermination(timeout, unit)) return false;
315 if (!repositoryService.getAdministrator().awaitTermination(timeout, unit)) return false;
316 return true;
317 }
318
319 /**
320 * Get a graph to the configuration content.
321 *
322 * @return a graph to the configuration content
323 */
324 protected Graph getConfigurationGraph() {
325 Graph result = Graph.create(configuration.getRepositorySource(), context);
326 if (configuration.getWorkspace() != null) {
327 result.useWorkspace(configuration.getWorkspace());
328 }
329 return result;
330 }
331
332 /**
333 * The component responsible for reading the configuration repository and (eventually) for propagating changes in the
334 * configuration repository into the services.
335 */
336 protected class ConfigurationScanner {
337 private final Problems problems;
338 private final ExecutionContext context;
339 private final DnaConfiguration.ConfigurationDefinition configurationRepository;
340
341 protected ConfigurationScanner( Problems problems,
342 ExecutionContext context,
343 DnaConfiguration.ConfigurationDefinition configurationRepository ) {
344 this.problems = problems;
345 this.context = context;
346 this.configurationRepository = configurationRepository;
347 }
348
349 public List<MimeTypeDetectorConfig> getMimeTypeDetectors() {
350 List<MimeTypeDetectorConfig> detectors = new ArrayList<MimeTypeDetectorConfig>();
351 Graph graph = Graph.create(configurationRepository.getRepositorySource(), context);
352 Path pathToSequencersNode = context.getValueFactories().getPathFactory().create(configurationRepository.getPath(),
353 DnaLexicon.MIME_TYPE_DETECTORS);
354 try {
355 Subgraph subgraph = graph.getSubgraphOfDepth(2).at(pathToSequencersNode);
356
357 Set<Name> skipProperties = new HashSet<Name>();
358 skipProperties.add(DnaLexicon.READABLE_NAME);
359 skipProperties.add(DnaLexicon.DESCRIPTION);
360 skipProperties.add(DnaLexicon.CLASSNAME);
361 skipProperties.add(DnaLexicon.CLASSPATH);
362 skipProperties.add(DnaLexicon.PATH_EXPRESSION);
363 Set<String> skipNamespaces = new HashSet<String>();
364 skipNamespaces.add(JcrLexicon.Namespace.URI);
365 skipNamespaces.add(JcrNtLexicon.Namespace.URI);
366 skipNamespaces.add(JcrMixLexicon.Namespace.URI);
367
368 for (Location detectorLocation : subgraph.getRoot().getChildren()) {
369 Node node = subgraph.getNode(detectorLocation);
370 String name = stringValueOf(node, DnaLexicon.READABLE_NAME);
371 if (name == null) name = stringValueOf(node);
372 String desc = stringValueOf(node, DnaLexicon.DESCRIPTION);
373 String classname = stringValueOf(node, DnaLexicon.CLASSNAME);
374 String[] classpath = stringValuesOf(node, DnaLexicon.CLASSPATH);
375 Map<String, Object> properties = new HashMap<String, Object>();
376 for (Property property : node.getProperties()) {
377 Name propertyName = property.getName();
378 if (skipNamespaces.contains(propertyName.getNamespaceUri())) continue;
379 if (skipProperties.contains(propertyName)) continue;
380 if (property.isSingle()) {
381 properties.put(propertyName.getLocalName(), property.getFirstValue());
382 } else {
383 properties.put(propertyName.getLocalName(), property.getValuesAsArray());
384 }
385 }
386 MimeTypeDetectorConfig config = new MimeTypeDetectorConfig(name, desc, properties, classname, classpath);
387 detectors.add(config);
388 }
389 } catch (PathNotFoundException e) {
390 // no detectors registered ...
391 }
392 return detectors;
393 }
394
395 public List<SequencerConfig> getSequencingConfigurations() {
396 List<SequencerConfig> configs = new ArrayList<SequencerConfig>();
397 Graph graph = Graph.create(configurationRepository.getRepositorySource(), context);
398 Path pathToSequencersNode = context.getValueFactories().getPathFactory().create(configurationRepository.getPath(),
399 DnaLexicon.SEQUENCERS);
400 try {
401 Subgraph subgraph = graph.getSubgraphOfDepth(2).at(pathToSequencersNode);
402
403 Set<Name> skipProperties = new HashSet<Name>();
404 skipProperties.add(DnaLexicon.READABLE_NAME);
405 skipProperties.add(DnaLexicon.DESCRIPTION);
406 skipProperties.add(DnaLexicon.CLASSNAME);
407 skipProperties.add(DnaLexicon.CLASSPATH);
408 skipProperties.add(DnaLexicon.PATH_EXPRESSION);
409 Set<String> skipNamespaces = new HashSet<String>();
410 skipNamespaces.add(JcrLexicon.Namespace.URI);
411 skipNamespaces.add(JcrNtLexicon.Namespace.URI);
412 skipNamespaces.add(JcrMixLexicon.Namespace.URI);
413
414 for (Location sequencerLocation : subgraph.getRoot().getChildren()) {
415 Node sequencerNode = subgraph.getNode(sequencerLocation);
416 String name = stringValueOf(sequencerNode, DnaLexicon.READABLE_NAME);
417 if (name == null) name = stringValueOf(sequencerNode);
418 String desc = stringValueOf(sequencerNode, DnaLexicon.DESCRIPTION);
419 String classname = stringValueOf(sequencerNode, DnaLexicon.CLASSNAME);
420 String[] classpath = stringValuesOf(sequencerNode, DnaLexicon.CLASSPATH);
421 String[] expressionStrings = stringValuesOf(sequencerNode, DnaLexicon.PATH_EXPRESSION);
422 List<PathExpression> pathExpressions = new ArrayList<PathExpression>();
423 if (expressionStrings != null) {
424 for (String expressionString : expressionStrings) {
425 try {
426 pathExpressions.add(PathExpression.compile(expressionString));
427 } catch (Throwable t) {
428 problems.addError(t,
429 RepositoryI18n.pathExpressionIsInvalidOnSequencer,
430 expressionString,
431 name,
432 t.getLocalizedMessage());
433 }
434 }
435 }
436 String[] goodExpressionStrings = new String[pathExpressions.size()];
437 for (int i = 0; i != pathExpressions.size(); ++i) {
438 PathExpression expression = pathExpressions.get(i);
439 goodExpressionStrings[i] = expression.getExpression();
440 }
441 Map<String, Object> properties = new HashMap<String, Object>();
442 for (Property property : sequencerNode.getProperties()) {
443 Name propertyName = property.getName();
444 if (skipNamespaces.contains(propertyName.getNamespaceUri())) continue;
445 if (skipProperties.contains(propertyName)) continue;
446 if (property.isSingle()) {
447 properties.put(propertyName.getLocalName(), property.getFirstValue());
448 } else {
449 properties.put(propertyName.getLocalName(), property.getValuesAsArray());
450 }
451 }
452 SequencerConfig config = new SequencerConfig(name, desc, properties, classname, classpath,
453 goodExpressionStrings);
454 configs.add(config);
455 }
456 } catch (PathNotFoundException e) {
457 // no detectors registered ...
458 }
459 return configs;
460 }
461
462 private String stringValueOf( Node node ) {
463 return node.getLocation().getPath().getLastSegment().getString(context.getNamespaceRegistry());
464 }
465
466 private String stringValueOf( Node node,
467 Name propertyName ) {
468 Property property = node.getProperty(propertyName);
469 if (property == null) return null;
470 if (property.isEmpty()) return null;
471 return context.getValueFactories().getStringFactory().create(property.getFirstValue());
472 }
473
474 private String[] stringValuesOf( Node node,
475 Name propertyName ) {
476 Property property = node.getProperty(propertyName);
477 if (property == null) return null;
478 return context.getValueFactories().getStringFactory().create(property.getValuesAsArray());
479 }
480
481 }
482 }