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    }