001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors.
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     * 
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.jcr;
025    
026    import java.io.File;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.net.URL;
030    import java.util.Collections;
031    import java.util.EnumMap;
032    import java.util.HashMap;
033    import java.util.HashSet;
034    import java.util.Map;
035    import java.util.Set;
036    import net.jcip.annotations.NotThreadSafe;
037    import org.jboss.dna.cnd.CndImporter;
038    import org.jboss.dna.common.component.ClassLoaderFactory;
039    import org.jboss.dna.common.util.CheckArg;
040    import org.jboss.dna.graph.ExecutionContext;
041    import org.jboss.dna.graph.Graph;
042    import org.jboss.dna.graph.Location;
043    import org.jboss.dna.graph.Node;
044    import org.jboss.dna.graph.Subgraph;
045    import org.jboss.dna.graph.connector.RepositorySource;
046    import org.jboss.dna.graph.io.Destination;
047    import org.jboss.dna.graph.io.GraphBatchDestination;
048    import org.jboss.dna.graph.property.Name;
049    import org.jboss.dna.graph.property.Path;
050    import org.jboss.dna.graph.property.PathNotFoundException;
051    import org.jboss.dna.graph.property.Property;
052    import org.jboss.dna.graph.property.NamespaceRegistry.Namespace;
053    import org.jboss.dna.jcr.JcrRepository.Option;
054    import org.jboss.dna.repository.DnaConfiguration;
055    import org.jboss.dna.repository.DnaConfigurationException;
056    import org.xml.sax.SAXException;
057    
058    /**
059     * A configuration builder for a {@link JcrEngine}. This class is an internal domain-specific language (DSL), and is designed to
060     * be used in a traditional way or in a method-chained manner:
061     * 
062     * <pre>
063     * configuration.repositorySource(&quot;Source1&quot;).setClass(InMemoryRepositorySource.class).setDescription(&quot;description&quot;);
064     * configuration.mimeTypeDetector(&quot;detector&quot;).setClass(ExtensionBasedMimeTypeDetector.class).setDescription(&quot;default detector&quot;);
065     * configuration.sequencer(&quot;MicrosoftDocs&quot;)
066     *              .setClass(&quot;org.jboss.dna.sequencer.msoffice.MSOfficeMetadataSequencer&quot;)
067     *              .setDescription(&quot;Our primary sequencer for all .doc files&quot;)
068     *              .sequencingFrom(&quot;/public//(*.(doc|xml|ppt)[*]/jcr:content[@jcr:data]&quot;)
069     *              .andOutputtingTo(&quot;/documents/$1&quot;);
070     * configuration.repository(&quot;MyRepository&quot;).setSource(&quot;Source1&quot;);
071     * configuration.save();
072     * </pre>
073     */
074    @NotThreadSafe
075    public class JcrConfiguration extends DnaConfiguration {
076    
077        /**
078         * Interface used to define a JCR Repository that's accessible from the JcrEngine.
079         * 
080         * @param <ReturnType>
081         */
082        public interface RepositoryDefinition<ReturnType> extends Returnable<ReturnType>, Removable<ReturnType> {
083    
084            /**
085             * Specify the name of the repository source that is to be used by this JCR repository.
086             * 
087             * @param sourceName the name of the repository source that should be exposed by this JCR repository
088             * @return the interface used to set the value for the property; never null
089             * @throws IllegalArgumentException if the source name parameter is null
090             */
091            RepositoryDefinition<ReturnType> setSource( String sourceName );
092    
093            /**
094             * Get the name of the repository source that is to be used by this JCR repository.
095             * 
096             * @return the source name, or null if it has not yet been set
097             */
098            String getSource();
099    
100            /**
101             * Specify the repository option that is to be set.
102             * 
103             * @param option the option to be set
104             * @param value the new value for the option
105             * @return the interface used to set the value for the property; never null
106             * @throws IllegalArgumentException if either parameter is null
107             */
108            RepositoryDefinition<ReturnType> setOption( JcrRepository.Option option,
109                                                        String value );
110    
111            /**
112             * Get the value for the repository option.
113             * 
114             * @param option the option
115             * @return the current option value, which may be null if the option has not been set (and its default would be used)
116             * @throws IllegalArgumentException if the option parameter is null
117             */
118            String getOption( JcrRepository.Option option );
119    
120            /**
121             * Specify that the CND file located at the supplied path should be loaded into the repository.
122             * 
123             * @param pathToCndFile the path to the CND file
124             * @return this object for chained method invocation
125             * @throws IllegalArgumentException if the string is null or empty
126             * @throws DnaConfigurationException if there is an error reading the CND file
127             */
128            RepositoryDefinition<ReturnType> addNodeTypes( String pathToCndFile );
129    
130            /**
131             * Specify that the CND file is to be loaded into the repository.
132             * 
133             * @param cndFile the CND file
134             * @return this object for chained method invocation
135             * @throws IllegalArgumentException if the file is null
136             * @throws DnaConfigurationException if there is an error reading the file
137             */
138            RepositoryDefinition<ReturnType> addNodeTypes( File cndFile );
139    
140            /**
141             * Specify that the CND file is to be loaded into the repository.
142             * 
143             * @param urlOfCndFile the URL of the CND file
144             * @return this object for chained method invocation
145             * @throws IllegalArgumentException if the URL is null
146             * @throws DnaConfigurationException if there is an error reading the content at the URL
147             */
148            RepositoryDefinition<ReturnType> addNodeTypes( URL urlOfCndFile );
149    
150            /**
151             * Specify that the CND file is to be loaded into the repository.
152             * 
153             * @param cndContent the stream containing the CND content
154             * @return this object for chained method invocation
155             * @throws IllegalArgumentException if the URL is null
156             * @throws DnaConfigurationException if there is an error reading the stream at the URL
157             */
158            RepositoryDefinition<ReturnType> addNodeTypes( InputStream cndContent );
159    
160            /**
161             * Specify the namespace binding that should be made available in this repository.
162             * 
163             * @param prefix the namespace prefix; may not be null or empty, and must be a valid prefix
164             * @param uri the uri for the namespace; may not be null or empty
165             * @return the interface used to set the value for the property; never null
166             */
167            RepositoryDefinition<ReturnType> registerNamespace( String prefix,
168                                                                String uri );
169        }
170    
171        private final Map<String, RepositoryDefinition<? extends JcrConfiguration>> repositoryDefinitions = new HashMap<String, RepositoryDefinition<? extends JcrConfiguration>>();
172    
173        /**
174         * Create a new configuration, using a default-constructed {@link ExecutionContext}.
175         */
176        public JcrConfiguration() {
177            super();
178        }
179    
180        /**
181         * Create a new configuration using the supplied {@link ExecutionContext}.
182         * 
183         * @param context the execution context
184         * @throws IllegalArgumentException if the path is null or empty
185         */
186        public JcrConfiguration( ExecutionContext context ) {
187            super(context);
188        }
189    
190        /**
191         * {@inheritDoc}
192         * 
193         * @throws IOException
194         * @throws SAXException
195         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.lang.String)
196         */
197        @Override
198        public JcrConfiguration loadFrom( String pathToFile ) throws IOException, SAXException {
199            super.loadFrom(pathToFile);
200            return this;
201        }
202    
203        /**
204         * {@inheritDoc}
205         * 
206         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.lang.String, java.lang.String)
207         */
208        @Override
209        public JcrConfiguration loadFrom( String pathToConfigurationFile,
210                                          String path ) throws IOException, SAXException {
211            super.loadFrom(pathToConfigurationFile, path);
212            return this;
213        }
214    
215        /**
216         * {@inheritDoc}
217         * 
218         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.io.File)
219         */
220        @Override
221        public JcrConfiguration loadFrom( File configurationFile ) throws IOException, SAXException {
222            super.loadFrom(configurationFile);
223            return this;
224        }
225    
226        /**
227         * {@inheritDoc}
228         * 
229         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.io.File, java.lang.String)
230         */
231        @Override
232        public JcrConfiguration loadFrom( File configurationFile,
233                                          String path ) throws IOException, SAXException {
234            super.loadFrom(configurationFile, path);
235            return this;
236        }
237    
238        /**
239         * {@inheritDoc}
240         * 
241         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.net.URL)
242         */
243        @Override
244        public JcrConfiguration loadFrom( URL urlToConfigurationFile ) throws IOException, SAXException {
245            super.loadFrom(urlToConfigurationFile);
246            return this;
247        }
248    
249        /**
250         * {@inheritDoc}
251         * 
252         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.net.URL, java.lang.String)
253         */
254        @Override
255        public JcrConfiguration loadFrom( URL urlToConfigurationFile,
256                                          String path ) throws IOException, SAXException {
257            super.loadFrom(urlToConfigurationFile, path);
258            return this;
259        }
260    
261        /**
262         * {@inheritDoc}
263         * 
264         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.io.InputStream)
265         */
266        @Override
267        public JcrConfiguration loadFrom( InputStream configurationFileInputStream ) throws IOException, SAXException {
268            super.loadFrom(configurationFileInputStream);
269            return this;
270        }
271    
272        /**
273         * {@inheritDoc}
274         * 
275         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(java.io.InputStream, java.lang.String)
276         */
277        @Override
278        public JcrConfiguration loadFrom( InputStream configurationFileInputStream,
279                                          String path ) throws IOException, SAXException {
280            super.loadFrom(configurationFileInputStream, path);
281            return this;
282        }
283    
284        /**
285         * {@inheritDoc}
286         * 
287         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(org.jboss.dna.graph.connector.RepositorySource)
288         */
289        @Override
290        public JcrConfiguration loadFrom( RepositorySource source ) {
291            super.loadFrom(source);
292            return this;
293        }
294    
295        /**
296         * {@inheritDoc}
297         * 
298         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(org.jboss.dna.graph.connector.RepositorySource, java.lang.String)
299         */
300        @Override
301        public JcrConfiguration loadFrom( RepositorySource source,
302                                          String workspaceName ) {
303            super.loadFrom(source, workspaceName);
304            return this;
305        }
306    
307        /**
308         * {@inheritDoc}
309         * 
310         * @see org.jboss.dna.repository.DnaConfiguration#loadFrom(org.jboss.dna.graph.connector.RepositorySource, java.lang.String,
311         *      java.lang.String)
312         */
313        @Override
314        public JcrConfiguration loadFrom( RepositorySource source,
315                                          String workspaceName,
316                                          String pathInWorkspace ) {
317            super.loadFrom(source, workspaceName, pathInWorkspace);
318            return this;
319        }
320    
321        /**
322         * {@inheritDoc}
323         * 
324         * @see org.jboss.dna.repository.DnaConfiguration#and()
325         */
326        @Override
327        public JcrConfiguration and() {
328            return this;
329        }
330    
331        /**
332         * {@inheritDoc}
333         * 
334         * @see org.jboss.dna.repository.DnaConfiguration#withClassLoaderFactory(org.jboss.dna.common.component.ClassLoaderFactory)
335         */
336        @Override
337        public JcrConfiguration withClassLoaderFactory( ClassLoaderFactory classLoaderFactory ) {
338            super.withClassLoaderFactory(classLoaderFactory);
339            return this;
340        }
341    
342        /**
343         * {@inheritDoc}
344         * 
345         * @see org.jboss.dna.repository.DnaConfiguration#mimeTypeDetector(java.lang.String)
346         */
347        @Override
348        public MimeTypeDetectorDefinition<JcrConfiguration> mimeTypeDetector( String name ) {
349            return mimeTypeDetectorDefinition(this, name);
350        }
351    
352        /**
353         * {@inheritDoc}
354         * 
355         * @see org.jboss.dna.repository.DnaConfiguration#repositorySource(java.lang.String)
356         */
357        @Override
358        public RepositorySourceDefinition<JcrConfiguration> repositorySource( String name ) {
359            return repositorySourceDefinition(this, name);
360        }
361    
362        /**
363         * {@inheritDoc}
364         * 
365         * @see org.jboss.dna.repository.DnaConfiguration#sequencer(java.lang.String)
366         */
367        @Override
368        public SequencerDefinition<JcrConfiguration> sequencer( String name ) {
369            return sequencerDefinition(this, name);
370        }
371    
372        /**
373         * Obtain or create a definition for the {@link javax.jcr.Repository JCR Repository} with the supplied name or identifier. A
374         * new definition will be created if there currently is no sequencer defined with the supplied name.
375         * 
376         * @param name the name or identifier of the sequencer
377         * @return the details of the sequencer definition; never null
378         */
379        public RepositoryDefinition<JcrConfiguration> repository( String name ) {
380            return repositoryDefinition(this, name);
381        }
382    
383        /**
384         * Get the list of sequencer definitions.
385         * 
386         * @return the unmodifiable set of definitions; never null but possibly empty if there are no definitions
387         */
388        public Set<RepositoryDefinition<JcrConfiguration>> repositories() {
389            // Get the children under the 'dna:mimeTypeDetectors' node ...
390            Set<String> names = getNamesOfComponentsUnder(DnaLexicon.REPOSITORIES);
391            names.addAll(this.repositoryDefinitions.keySet());
392            Set<RepositoryDefinition<JcrConfiguration>> results = new HashSet<RepositoryDefinition<JcrConfiguration>>();
393            for (String name : names) {
394                results.add(repository(name));
395            }
396            return Collections.unmodifiableSet(results);
397        }
398    
399        /**
400         * {@inheritDoc}
401         * 
402         * @see org.jboss.dna.repository.DnaConfiguration#save()
403         */
404        @Override
405        public JcrConfiguration save() {
406            super.save();
407            this.repositoryDefinitions.clear();
408            return this;
409        }
410    
411        /**
412         * {@inheritDoc}
413         * 
414         * @see org.jboss.dna.repository.DnaConfiguration#build()
415         */
416        @Override
417        public JcrEngine build() {
418            save();
419            return new JcrEngine(getExecutionContext(), getConfigurationDefinition());
420        }
421    
422        /**
423         * Utility method to construct a definition object for the repository with the supplied name and return type.
424         * 
425         * @param <ReturnType> the type of the return object
426         * @param returnObject the return object
427         * @param name the name of the repository
428         * @return the definition for the repository
429         */
430        @SuppressWarnings( "unchecked" )
431        protected <ReturnType extends JcrConfiguration> RepositoryDefinition<ReturnType> repositoryDefinition( ReturnType returnObject,
432                                                                                                               String name ) {
433            RepositoryDefinition<ReturnType> definition = (RepositoryDefinition<ReturnType>)repositoryDefinitions.get(name);
434            if (definition == null) {
435                definition = new RepositoryBuilder<ReturnType>(returnObject, changes(), path(), DnaLexicon.REPOSITORIES, name(name));
436                repositoryDefinitions.put(name, definition);
437            }
438            return definition;
439        }
440    
441        protected class RepositoryBuilder<ReturnType> extends GraphReturnable<ReturnType, RepositoryDefinition<ReturnType>>
442            implements RepositoryDefinition<ReturnType> {
443            private final EnumMap<JcrRepository.Option, String> optionValues = new EnumMap<Option, String>(Option.class);
444    
445            protected RepositoryBuilder( ReturnType returnObject,
446                                         Graph.Batch batch,
447                                         Path path,
448                                         Name... names ) {
449                super(returnObject, batch, path, names);
450                // Load the current options ...
451                try {
452                    Path optionsPath = context.getValueFactories().getPathFactory().create(path, DnaLexicon.OPTIONS);
453                    Subgraph options = batch.getGraph().getSubgraphOfDepth(2).at(optionsPath);
454                    for (Location optionChild : options.getRoot().getChildren()) {
455                        Node option = options.getNode(optionChild);
456                        Property property = option.getProperty(DnaLexicon.VALUE);
457                        if (property != null && property.isEmpty()) {
458                            try {
459                                Option key = Option.findOption(optionChild.getPath()
460                                                                          .getLastSegment()
461                                                                          .getString(context.getNamespaceRegistry()));
462                                String value = context.getValueFactories().getStringFactory().create(property.getFirstValue());
463                                optionValues.put(key, value);
464                            } catch (IllegalArgumentException e) {
465                                // the key is not valid, so skip it ...
466                            }
467                        }
468                    }
469                } catch (PathNotFoundException e) {
470                    // No current options
471                }
472            }
473    
474            @Override
475            protected RepositoryDefinition<ReturnType> thisType() {
476                return this;
477            }
478    
479            public RepositoryDefinition<ReturnType> setSource( String sourceName ) {
480                setProperty(DnaLexicon.SOURCE_NAME, sourceName);
481                return this;
482            }
483    
484            public String getSource() {
485                Property property = getProperty(DnaLexicon.SOURCE_NAME);
486                if (property != null && !property.isEmpty()) {
487                    return context.getValueFactories().getStringFactory().create(property.getFirstValue());
488                }
489                return null;
490            }
491    
492            public RepositoryDefinition<ReturnType> setOption( JcrRepository.Option option,
493                                                               String value ) {
494                CheckArg.isNotNull(option, "option");
495                CheckArg.isNotNull(value, "value");
496                createIfMissing(DnaLexicon.OPTIONS, option.name()).with(DnaLexicon.VALUE, value.trim()).and();
497                optionValues.put(option, value);
498                return this;
499            }
500    
501            public String getOption( Option option ) {
502                CheckArg.isNotNull(option, "option");
503                return optionValues.get(option);
504            }
505    
506            public RepositoryDefinition<ReturnType> registerNamespace( String prefix,
507                                                                       String uri ) {
508                CheckArg.isNotEmpty(prefix, "prefix");
509                CheckArg.isNotEmpty(uri, "uri");
510                prefix = prefix.trim();
511                uri = uri.trim();
512                createIfMissing(DnaLexicon.NAMESPACES, prefix).with(DnaLexicon.URI, uri).and();
513                return this;
514            }
515    
516            public RepositoryDefinition<ReturnType> addNodeTypes( String pathToCndFile ) {
517                CheckArg.isNotEmpty(pathToCndFile, "pathToCndFile");
518                return addNodeTypes(new File(pathToCndFile));
519            }
520    
521            public RepositoryDefinition<ReturnType> addNodeTypes( File file ) {
522                CheckArg.isNotNull(file, "file");
523                if (file.exists() && file.canRead()) {
524                    CndImporter importer = createCndImporter();
525                    try {
526                        Set<Namespace> namespacesBefore = batch.getGraph().getContext().getNamespaceRegistry().getNamespaces();
527                        importer.importFrom(file, getProblems());
528    
529                        // Record any new namespaces added by this import ...
530                        registerNewNamespaces(namespacesBefore);
531                    } catch (IOException e) {
532                        throw new DnaConfigurationException(e);
533                    }
534                    return this;
535                }
536                throw new DnaConfigurationException(JcrI18n.fileDoesNotExist.text(file.getPath()));
537            }
538    
539            public RepositoryDefinition<ReturnType> addNodeTypes( URL url ) {
540                CheckArg.isNotNull(url, "url");
541                // Obtain the stream ...
542                InputStream stream = null;
543                boolean foundError = false;
544                try {
545                    Set<Namespace> namespacesBefore = batch.getGraph().getContext().getNamespaceRegistry().getNamespaces();
546                    stream = url.openStream();
547                    CndImporter importer = createCndImporter();
548                    importer.importFrom(stream, getProblems(), url.toString());
549    
550                    // Record any new namespaces added by this import ...
551                    registerNewNamespaces(namespacesBefore);
552                } catch (IOException e) {
553                    foundError = true;
554                    throw new DnaConfigurationException(e);
555                } finally {
556                    if (stream != null) {
557                        try {
558                            stream.close();
559                        } catch (IOException e) {
560                            if (!foundError) {
561                                throw new DnaConfigurationException(e);
562                            }
563                        }
564                    }
565                }
566                return this;
567            }
568    
569            public RepositoryDefinition<ReturnType> addNodeTypes( InputStream cndContent ) {
570                CndImporter importer = createCndImporter();
571                try {
572                    Set<Namespace> namespacesBefore = batch.getGraph().getContext().getNamespaceRegistry().getNamespaces();
573                    importer.importFrom(cndContent, getProblems(), "stream");
574    
575                    // Record any new namespaces added by this import ...
576                    registerNewNamespaces(namespacesBefore);
577                } catch (IOException e) {
578                    throw new DnaConfigurationException(e);
579                }
580                return this;
581            }
582    
583            protected void registerNewNamespaces( Set<Namespace> namespacesBefore ) {
584                Set<Namespace> namespacesAfter = batch.getGraph().getContext().getNamespaceRegistry().getNamespaces();
585                Set<Namespace> newNamespaces = new HashSet<Namespace>(namespacesAfter);
586                newNamespaces.removeAll(namespacesBefore);
587                for (Namespace namespace : newNamespaces) {
588                    registerNamespace(namespace.getPrefix(), namespace.getNamespaceUri());
589                }
590            }
591    
592            protected CndImporter createCndImporter() {
593                // The node types will be loaded into 'dna:repositories/{repositoryName}/dna:nodeTypes/' ...
594                Path nodeTypesPath = subpath(DnaLexicon.NODE_TYPES);
595                createIfMissing(DnaLexicon.NODE_TYPES).and();
596    
597                // Now set up the destination, but make it so that ...
598                Destination destination = new GraphBatchDestination(batch, true); // will NOT be executed
599    
600                // And create the importer that will load the CND content into the repository ...
601                return new CndImporter(destination, nodeTypesPath);
602            }
603        }
604    
605    }