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.graph.util;
023    
024    import java.io.File;
025    import java.io.IOException;
026    import java.io.InputStream;
027    import java.net.MalformedURLException;
028    import java.net.URI;
029    import java.util.ArrayList;
030    import java.util.Collection;
031    import java.util.HashMap;
032    import java.util.HashSet;
033    import java.util.Iterator;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Set;
037    import net.jcip.annotations.Immutable;
038    import org.jboss.dna.common.i18n.I18n;
039    import org.jboss.dna.common.monitor.ProgressMonitor;
040    import org.jboss.dna.common.monitor.SimpleProgressMonitor;
041    import org.jboss.dna.common.util.CheckArg;
042    import org.jboss.dna.common.util.Logger;
043    import org.jboss.dna.graph.ExecutionContext;
044    import org.jboss.dna.graph.GraphI18n;
045    import org.jboss.dna.graph.commands.CompositeCommand;
046    import org.jboss.dna.graph.commands.GraphCommand;
047    import org.jboss.dna.graph.commands.NodeConflictBehavior;
048    import org.jboss.dna.graph.commands.basic.BasicCreateNodeCommand;
049    import org.jboss.dna.graph.commands.basic.BasicGraphCommand;
050    import org.jboss.dna.graph.connectors.RepositoryConnection;
051    import org.jboss.dna.graph.connectors.RepositoryConnectionFactory;
052    import org.jboss.dna.graph.connectors.RepositorySource;
053    import org.jboss.dna.graph.connectors.RepositorySourceException;
054    import org.jboss.dna.graph.properties.Name;
055    import org.jboss.dna.graph.properties.NameFactory;
056    import org.jboss.dna.graph.properties.NamespaceRegistry;
057    import org.jboss.dna.graph.properties.Path;
058    import org.jboss.dna.graph.properties.PathFactory;
059    import org.jboss.dna.graph.properties.Property;
060    import org.jboss.dna.graph.properties.PropertyFactory;
061    import org.jboss.dna.graph.properties.Reference;
062    import org.jboss.dna.graph.properties.ValueFactories;
063    import org.jboss.dna.graph.properties.ValueFactory;
064    import org.jboss.dna.graph.properties.ValueFormatException;
065    import org.jboss.dna.graph.sequencers.SequencerContext;
066    import org.jboss.dna.graph.sequencers.SequencerOutput;
067    import org.jboss.dna.graph.sequencers.StreamSequencer;
068    import org.jboss.dna.graph.xml.DnaXmlLexicon;
069    import org.jboss.dna.graph.xml.XmlSequencer;
070    
071    /**
072     * @author Randall Hauch
073     */
074    public class GraphImporter {
075    
076        public interface ImportSpecification {
077            /**
078             * Specify the location where the content is to be imported, and then perform the import. This is equivalent to calling
079             * <code>{@link #into(String, Path) into(sourceName,rootPath)}</code>.
080             * 
081             * @param sourceName the name of the source into which the content is to be imported
082             * @throws IllegalArgumentException if the <code>uri</code> or path are null
083             * @throws IOException if there is a problem reading the content
084             * @throws RepositorySourceException if there is a problem while writing the content to the {@link RepositorySource
085             *         repository source}
086             */
087            void into( String sourceName ) throws IOException, RepositorySourceException;
088    
089            /**
090             * Specify the location where the content is to be imported, and then perform the import.
091             * 
092             * @param sourceName the name of the source into which the content is to be imported
093             * @param pathInSource the path in the {@link RepositorySource repository source} named <code>sourceName</code> where the
094             *        content is to be written; may not be null
095             * @throws IllegalArgumentException if the <code>uri</code> or path are null
096             * @throws IOException if there is a problem reading the content
097             * @throws RepositorySourceException if there is a problem while writing the content to the {@link RepositorySource
098             *         repository source}
099             */
100            void into( String sourceName,
101                       Path pathInSource ) throws IOException, RepositorySourceException;
102        }
103    
104        @Immutable
105        protected abstract class ImportedContentUsingSequencer implements ImportSpecification {
106            private final StreamSequencer sequencer;
107    
108            protected ImportedContentUsingSequencer( StreamSequencer sequencer ) {
109                this.sequencer = sequencer;
110            }
111    
112            protected StreamSequencer getSequencer() {
113                return this.sequencer;
114            }
115    
116            protected NodeConflictBehavior getConflictBehavior() {
117                return NodeConflictBehavior.UPDATE;
118            }
119    
120            /**
121             * {@inheritDoc}
122             * 
123             * @see org.jboss.dna.graph.util.GraphImporter.ImportSpecification#into(java.lang.String)
124             */
125            public void into( String sourceName ) throws IOException, RepositorySourceException {
126                Path root = getContext().getValueFactories().getPathFactory().createRootPath();
127                into(sourceName, root);
128            }
129        }
130    
131        @Immutable
132        protected class UriImportedContent extends ImportedContentUsingSequencer {
133            private final URI uri;
134            private final String mimeType;
135    
136            protected UriImportedContent( StreamSequencer sequencer,
137                                          URI uri,
138                                          String mimeType ) {
139                super(sequencer);
140                this.uri = uri;
141                this.mimeType = mimeType;
142            }
143    
144            /**
145             * @return mimeType
146             */
147            public String getMimeType() {
148                return mimeType;
149            }
150    
151            /**
152             * @return uri
153             */
154            public URI getUri() {
155                return uri;
156            }
157    
158            /**
159             * {@inheritDoc}
160             * 
161             * @see org.jboss.dna.graph.util.GraphImporter.ImportSpecification#into(java.lang.String,
162             *      org.jboss.dna.graph.properties.Path)
163             */
164            public void into( String sourceName,
165                              Path pathInSource ) throws IOException, RepositorySourceException {
166                CheckArg.isNotNull(sourceName, "sourceName");
167                CheckArg.isNotNull(pathInSource, "pathInSource");
168                importWithSequencer(getSequencer(), uri, mimeType, sourceName, pathInSource, getConflictBehavior());
169            }
170        }
171    
172        private final RepositoryConnectionFactory sources;
173        private final ExecutionContext context;
174    
175        public GraphImporter( RepositoryConnectionFactory sources,
176                              ExecutionContext context ) {
177            CheckArg.isNotNull(sources, "sources");
178            CheckArg.isNotNull(context, "context");
179            this.sources = sources;
180            this.context = context;
181        }
182    
183        /**
184         * Get the context in which the importer will be executed.
185         * 
186         * @return the execution context; never null
187         */
188        public ExecutionContext getContext() {
189            return this.context;
190        }
191    
192        /**
193         * Import the content from the XML file at the supplied URI, specifying on the returned {@link ImportSpecification} where the
194         * content is to be imported.
195         * 
196         * @param uri the URI where the importer can read the content that is to be imported
197         * @return the object that should be used to specify into which the content is to be imported
198         * @throws IllegalArgumentException if the <code>uri</code> or destination path are null
199         */
200        public ImportSpecification importXml( URI uri ) {
201            CheckArg.isNotNull(uri, "uri");
202    
203            // Create the sequencer ...
204            StreamSequencer sequencer = new XmlSequencer();
205            return new UriImportedContent(sequencer, uri, "text/xml");
206        }
207    
208        /**
209         * Import the content from the XML file at the supplied file location, specifying on the returned {@link ImportSpecification}
210         * where the content is to be imported.
211         * 
212         * @param pathToFile the path to the XML file that should be imported.
213         * @return the object that should be used to specify into which the content is to be imported
214         * @throws IllegalArgumentException if the <code>uri</code> or destination path are null
215         */
216        public ImportSpecification importXml( String pathToFile ) {
217            CheckArg.isNotNull(pathToFile, "pathToFile");
218            return importXml(new File(pathToFile).toURI());
219        }
220    
221        /**
222         * Import the content from the supplied XML file, specifying on the returned {@link ImportSpecification} where the content is
223         * to be imported.
224         * 
225         * @param file the XML file that should be imported.
226         * @return the object that should be used to specify into which the content is to be imported
227         * @throws IllegalArgumentException if the <code>uri</code> or destination path are null
228         */
229        public ImportSpecification importXml( File file ) {
230            CheckArg.isNotNull(file, "file");
231            return importXml(file.toURI());
232        }
233    
234        /**
235         * Read the content from the supplied URI and import into the repository at the supplied location.
236         * 
237         * @param uri the URI where the importer can read the content that is to be imported
238         * @param sourceName the name of the source into which the content is to be imported
239         * @param destinationPathInSource the path in the {@link RepositorySource repository source} where the content is to be
240         *        written; may not be null
241         * @throws IllegalArgumentException if the <code>uri</code> or destination path are null
242         * @throws IOException if there is a problem reading the content
243         * @throws RepositorySourceException if there is a problem while writing the content to the {@link RepositorySource repository
244         *         source}
245         */
246        public void importXml( URI uri,
247                               String sourceName,
248                               Path destinationPathInSource ) throws IOException, RepositorySourceException {
249            CheckArg.isNotNull(uri, "uri");
250            CheckArg.isNotNull(destinationPathInSource, "destinationPathInSource");
251    
252            // Create the sequencer ...
253            StreamSequencer sequencer = new XmlSequencer();
254            importWithSequencer(sequencer, uri, "text/xml", sourceName, destinationPathInSource, NodeConflictBehavior.UPDATE);
255        }
256    
257        /**
258         * Use the supplied sequencer to read the content at the given URI (with the specified MIME type) and write that content to
259         * the {@link RepositorySource repository source} into the specified location.
260         * 
261         * @param sequencer the sequencer that should be used; may not be null
262         * @param contentUri the URI where the content can be found; may not be null
263         * @param mimeType the MIME type for the content; may not be null
264         * @param sourceName the name of the source into which the content is to be imported
265         * @param destinationPathInSource the path in the {@link RepositorySource repository source} where the content is to be
266         *        written; may not be null
267         * @param conflictBehavior the behavior when a node is to be created when an existing node already exists; defaults to
268         *        {@link NodeConflictBehavior#UPDATE} if null
269         * @throws IOException if there is a problem reading the content
270         * @throws RepositorySourceException if there is a problem while writing the content to the {@link RepositorySource repository
271         *         source}
272         */
273        protected void importWithSequencer( StreamSequencer sequencer,
274                                            URI contentUri,
275                                            String mimeType,
276                                            String sourceName,
277                                            Path destinationPathInSource,
278                                            NodeConflictBehavior conflictBehavior ) throws IOException, RepositorySourceException {
279            assert sequencer != null;
280            assert contentUri != null;
281            assert mimeType != null;
282            assert sourceName != null;
283            assert destinationPathInSource != null;
284            conflictBehavior = conflictBehavior != null ? conflictBehavior : NodeConflictBehavior.UPDATE;
285    
286            // Get the input path by creating from the URI, in case the URI is a valid path ...
287            Path inputPath = extractInputPathFrom(contentUri);
288            assert inputPath != null;
289    
290            // Now create the importer context ...
291            PropertyFactory propertyFactory = getContext().getPropertyFactory();
292            NameFactory nameFactory = getContext().getValueFactories().getNameFactory();
293            Set<Property> inputProperties = new HashSet<Property>();
294            inputProperties.add(propertyFactory.create(nameFactory.create("jcr:mimeType"), mimeType));
295            ImporterContext importerContext = new ImporterContext(inputPath, inputProperties, "text/xml");
296    
297            // Now run the sequencer ...
298            String activity = GraphI18n.errorImportingContent.text(destinationPathInSource, contentUri);
299            ProgressMonitor progressMonitor = new SimpleProgressMonitor(activity);
300            ImporterCommands commands = new ImporterCommands(destinationPathInSource, conflictBehavior);
301            InputStream stream = null;
302            try {
303                stream = contentUri.toURL().openStream();
304                sequencer.sequence(stream, commands, importerContext, progressMonitor);
305            } catch (MalformedURLException err) {
306                throw new IOException(err.getMessage());
307            } finally {
308                if (stream != null) {
309                    try {
310                        stream.close();
311                    } catch (IOException e) {
312                        I18n msg = GraphI18n.errorImportingContent;
313                        context.getLogger(getClass()).error(e, msg, mimeType, contentUri);
314                    }
315                }
316            }
317    
318            // Now execute the commands against the repository ...
319            RepositoryConnection connection = null;
320            try {
321                connection = sources.createConnection(sourceName);
322                if (connection == null) {
323                    I18n msg = GraphI18n.unableToFindRepositorySourceWithName;
324                    throw new RepositorySourceException(msg.text(sourceName));
325                }
326                connection.execute(context, commands);
327            } finally {
328                if (connection != null) {
329                    try {
330                        connection.close();
331                    } catch (RepositorySourceException e) {
332                        I18n msg = GraphI18n.errorImportingContent;
333                        context.getLogger(getClass()).error(e, msg, mimeType, contentUri);
334                    }
335                }
336            }
337        }
338    
339        /**
340         * @param contentUri
341         * @return the input path
342         */
343        protected Path extractInputPathFrom( URI contentUri ) {
344            try {
345                return getContext().getValueFactories().getPathFactory().create(contentUri);
346            } catch (ValueFormatException e) {
347                // Get the last component of the URI, and use it to create the input path ...
348                String path = contentUri.getPath();
349                return getContext().getValueFactories().getPathFactory().create(path);
350            }
351        }
352    
353        protected class SingleRepositorySourceConnectionFactory implements RepositoryConnectionFactory {
354            private final RepositorySource source;
355    
356            protected SingleRepositorySourceConnectionFactory( RepositorySource source ) {
357                CheckArg.isNotNull(source, "source");
358                this.source = source;
359            }
360    
361            /**
362             * {@inheritDoc}
363             * 
364             * @see org.jboss.dna.graph.connectors.RepositoryConnectionFactory#createConnection(java.lang.String)
365             */
366            public RepositoryConnection createConnection( String sourceName ) throws RepositorySourceException {
367                if (source.getName().equals(sourceName)) {
368                    return source.getConnection();
369                }
370                return null;
371            }
372        }
373    
374        protected class ImporterCommands extends BasicGraphCommand implements SequencerOutput, CompositeCommand {
375            private final List<GraphCommand> commands = new ArrayList<GraphCommand>();
376            private final Map<Path, BasicCreateNodeCommand> createNodeCommands = new HashMap<Path, BasicCreateNodeCommand>();
377            private final NodeConflictBehavior conflictBehavior;
378            private final Path destinationPath;
379            private final NameFactory nameFactory;
380            private final Name primaryTypeName;
381    
382            protected ImporterCommands( Path destinationPath,
383                                        NodeConflictBehavior conflictBehavior ) {
384                CheckArg.isNotNull(destinationPath, "destinationPath");
385                CheckArg.isNotNull(conflictBehavior, "conflictBehavior");
386                this.conflictBehavior = conflictBehavior;
387                this.destinationPath = destinationPath;
388                this.nameFactory = getContext().getValueFactories().getNameFactory();
389                this.primaryTypeName = this.nameFactory.create("jcr:primaryType");
390            }
391    
392            /**
393             * {@inheritDoc}
394             * 
395             * @see org.jboss.dna.graph.sequencers.SequencerOutput#getFactories()
396             */
397            public ValueFactories getFactories() {
398                return getContext().getValueFactories();
399            }
400    
401            /**
402             * {@inheritDoc}
403             * 
404             * @see org.jboss.dna.graph.sequencers.SequencerOutput#getNamespaceRegistry()
405             */
406            public NamespaceRegistry getNamespaceRegistry() {
407                return getContext().getNamespaceRegistry();
408            }
409    
410            /**
411             * {@inheritDoc}
412             * 
413             * @see org.jboss.dna.graph.sequencers.SequencerOutput#setProperty(java.lang.String, java.lang.String, java.lang.Object[])
414             */
415            public void setProperty( String nodePath,
416                                     String propertyName,
417                                     Object... values ) {
418                // Create a command that sets the property ...
419                Path path = getFactories().getPathFactory().create(nodePath);
420                Name name = getFactories().getNameFactory().create(propertyName);
421                setProperty(path, name, values);
422            }
423    
424            /**
425             * {@inheritDoc}
426             * 
427             * @see org.jboss.dna.graph.sequencers.SequencerOutput#setProperty(org.jboss.dna.graph.properties.Path,
428             *      org.jboss.dna.graph.properties.Name, java.lang.Object[])
429             */
430            public void setProperty( Path nodePath,
431                                     Name propertyName,
432                                     Object... values ) {
433                // Ignore the property value if the "jcr:primaryType" is "dnaxml:document" ...
434                if (this.primaryTypeName.equals(propertyName) && values.length == 1) {
435                    Name typeName = this.nameFactory.create(values[0]);
436                    if (DnaXmlLexicon.DOCUMENT.equals(typeName)) return;
437                }
438                PathFactory pathFactory = getFactories().getPathFactory();
439                if (nodePath.isAbsolute()) nodePath.relativeTo(pathFactory.createRootPath());
440                nodePath = pathFactory.create(destinationPath, nodePath).getNormalizedPath();
441                Property property = getContext().getPropertyFactory().create(propertyName, values);
442                BasicCreateNodeCommand command = createNodeCommands.get(nodePath);
443                if (command != null) {
444                    // We've already created the node, so find that command and add to it.
445                    Collection<Property> properties = command.getProperties();
446                    // See if the property was already added and remove it if so
447                    Iterator<Property> iter = properties.iterator();
448                    while (iter.hasNext()) {
449                        Property existingProperty = iter.next();
450                        if (existingProperty.getName().equals(propertyName)) {
451                            iter.remove();
452                            break;
453                        }
454                    }
455                    command.getProperties().add(property);
456                } else {
457                    // We haven't created the node yet (and we're assuming that we need to), so create the node
458                    List<Property> properties = new ArrayList<Property>();
459                    properties.add(property);
460                    command = new BasicCreateNodeCommand(nodePath, properties, conflictBehavior);
461                    createNodeCommands.put(nodePath, command);
462                    commands.add(command);
463                }
464            }
465    
466            /**
467             * {@inheritDoc}
468             * 
469             * @see org.jboss.dna.graph.sequencers.SequencerOutput#setReference(java.lang.String, java.lang.String,
470             *      java.lang.String[])
471             */
472            public void setReference( String nodePath,
473                                      String propertyName,
474                                      String... paths ) {
475                Path path = getFactories().getPathFactory().create(nodePath);
476                Name name = getFactories().getNameFactory().create(propertyName);
477                // Create an array of reference values ...
478                ValueFactory<Reference> factory = getFactories().getReferenceFactory();
479                Object[] values = new Object[paths.length];
480                int i = 0;
481                for (String referencedPath : paths) {
482                    values[i++] = factory.create(referencedPath);
483                }
484                setProperty(path, name, values);
485            }
486    
487            /**
488             * {@inheritDoc}
489             * 
490             * @see java.lang.Iterable#iterator()
491             */
492            public Iterator<GraphCommand> iterator() {
493                return this.commands.iterator();
494            }
495    
496        }
497    
498        protected class ImporterContext implements SequencerContext {
499    
500            private final Path inputPath;
501            private final Set<Property> inputProperties;
502            private final String mimeType;
503    
504            protected ImporterContext( Path inputPath,
505                                       Set<Property> inputProperties,
506                                       String mimeType ) {
507                this.inputPath = inputPath;
508                this.inputProperties = inputProperties;
509                this.mimeType = mimeType;
510            }
511    
512            /**
513             * {@inheritDoc}
514             * 
515             * @see org.jboss.dna.graph.sequencers.SequencerContext#getFactories()
516             */
517            public ValueFactories getFactories() {
518                return getContext().getValueFactories();
519            }
520    
521            /**
522             * {@inheritDoc}
523             * 
524             * @see org.jboss.dna.graph.sequencers.SequencerContext#getInputPath()
525             */
526            public Path getInputPath() {
527                return inputPath;
528            }
529    
530            /**
531             * {@inheritDoc}
532             * 
533             * @see org.jboss.dna.graph.sequencers.SequencerContext#getInputProperties()
534             */
535            public Set<Property> getInputProperties() {
536                return inputProperties;
537            }
538    
539            /**
540             * {@inheritDoc}
541             * 
542             * @see org.jboss.dna.graph.sequencers.SequencerContext#getInputProperty(org.jboss.dna.graph.properties.Name)
543             */
544            public Property getInputProperty( Name name ) {
545                for (Property property : inputProperties) {
546                    if (property.getName().equals(name)) return property;
547                }
548                return null;
549            }
550    
551            /**
552             * {@inheritDoc}
553             * 
554             * @see org.jboss.dna.graph.sequencers.SequencerContext#getLogger(java.lang.Class)
555             */
556            public Logger getLogger( Class<?> clazz ) {
557                return getContext().getLogger(clazz);
558            }
559    
560            /**
561             * {@inheritDoc}
562             * 
563             * @see org.jboss.dna.graph.sequencers.SequencerContext#getLogger(java.lang.String)
564             */
565            public Logger getLogger( String name ) {
566                return getContext().getLogger(name);
567            }
568    
569            /**
570             * {@inheritDoc}
571             * 
572             * @see org.jboss.dna.graph.sequencers.SequencerContext#getMimeType()
573             */
574            public String getMimeType() {
575                return mimeType;
576            }
577    
578            /**
579             * {@inheritDoc}
580             * 
581             * @see org.jboss.dna.graph.sequencers.SequencerContext#getNamespaceRegistry()
582             */
583            public NamespaceRegistry getNamespaceRegistry() {
584                return getContext().getNamespaceRegistry();
585            }
586    
587        }
588    
589    }