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.maven.spi;
025    
026    import java.io.BufferedInputStream;
027    import java.io.BufferedOutputStream;
028    import java.io.ByteArrayInputStream;
029    import java.io.File;
030    import java.io.FileInputStream;
031    import java.io.FileNotFoundException;
032    import java.io.FileOutputStream;
033    import java.io.IOException;
034    import java.io.InputStream;
035    import java.io.OutputStream;
036    import java.net.MalformedURLException;
037    import java.net.URL;
038    import java.net.URLConnection;
039    import java.net.URLStreamHandler;
040    import java.util.Calendar;
041    import java.util.Properties;
042    import javax.jcr.Credentials;
043    import javax.jcr.ItemExistsException;
044    import javax.jcr.LoginException;
045    import javax.jcr.NoSuchWorkspaceException;
046    import javax.jcr.Node;
047    import javax.jcr.PathNotFoundException;
048    import javax.jcr.Property;
049    import javax.jcr.Repository;
050    import javax.jcr.RepositoryException;
051    import javax.jcr.Session;
052    import javax.jcr.SimpleCredentials;
053    import javax.jcr.lock.LockException;
054    import javax.jcr.nodetype.ConstraintViolationException;
055    import javax.jcr.nodetype.NoSuchNodeTypeException;
056    import javax.jcr.version.VersionException;
057    import org.jboss.dna.common.text.TextDecoder;
058    import org.jboss.dna.common.text.TextEncoder;
059    import org.jboss.dna.common.text.UrlEncoder;
060    import org.jboss.dna.common.util.Logger;
061    import org.jboss.dna.maven.ArtifactType;
062    import org.jboss.dna.maven.MavenI18n;
063    import org.jboss.dna.maven.MavenId;
064    import org.jboss.dna.maven.MavenRepositoryException;
065    import org.jboss.dna.maven.MavenUrl;
066    import org.jboss.dna.maven.SignatureType;
067    
068    /**
069     * Base class for providers that work against a JCR repository. This class implements all functionality except for creating the
070     * {@link Repository repository} instance, and it relies upon some other component or subclass to
071     * {@link #setRepository(Repository) set the repository instance}. Typically, this is done by a subclass in it's
072     * {@link #configure(Properties)} method:
073     * 
074     * <pre>
075     * public class MyCustomJcrMavenUrlProvider extends JcrMavenUrlProvider {
076     *     &#064;Override
077     *     public void configure(Properties properties) {
078     *          super.configure(properties);
079     *          properties = super.getProperties();     // always non-null
080     *          Repository repo = ...                   // Construct and configure
081     *          super.setRepository(repo);
082     *      }
083     * }
084     * </pre>
085     * 
086     * @author Randall Hauch
087     */
088    public class JcrMavenUrlProvider extends AbstractMavenUrlProvider {
089    
090        public static final String USERNAME = "dna.maven.urlprovider.username";
091        public static final String PASSWORD = "dna.maven.urlprovider.password";
092        public static final String WORKSPACE_NAME = "dna.maven.urlprovider.repository.workspace";
093        public static final String REPOSITORY_PATH = "dna.maven.urlprovider.repository.path";
094    
095        public static final String DEFAULT_PATH_TO_TOP_OF_MAVEN_REPOSITORY = "/dnaMavenRepository";
096        public static final String DEFAULT_CREATE_REPOSITORY_PATH = Boolean.TRUE.toString();
097    
098        public static final String CONTENT_NODE_NAME = "jcr:content";
099        public static final String CONTENT_PROPERTY_NAME = "jcr:data";
100    
101        private final URLStreamHandler urlStreamHandler = new JcrUrlStreamHandler();
102        private final TextEncoder urlEncoder;
103        private final TextDecoder urlDecoder;
104        private Repository repository;
105        private String workspaceName;
106        private Credentials credentials;
107        private String pathToTopOfRepository = DEFAULT_PATH_TO_TOP_OF_MAVEN_REPOSITORY;
108        private final Logger logger = Logger.getLogger(JcrMavenUrlProvider.class);
109    
110        /**
111         * 
112         */
113        public JcrMavenUrlProvider() {
114            UrlEncoder encoder = new UrlEncoder().setSlashEncoded(false);
115            this.urlEncoder = encoder;
116            this.urlDecoder = encoder;
117        }
118    
119        /**
120         * {@inheritDoc}
121         */
122        @Override
123        public void configure( Properties properties ) {
124            super.configure(properties);
125            properties = super.getProperties();
126            String username = properties.getProperty(USERNAME);
127            if (username != null) {
128                String password = properties.getProperty(PASSWORD, "");
129                this.setCredentials(new SimpleCredentials(username, password.toCharArray()));
130            }
131            this.setWorkspaceName(properties.getProperty(WORKSPACE_NAME, this.getWorkspaceName()));
132            this.setPathToTopOfRepository(properties.getProperty(REPOSITORY_PATH, this.getPathToTopOfRepository()));
133        }
134    
135        /**
136         * @return credentials
137         */
138        public Credentials getCredentials() {
139            return this.credentials;
140        }
141    
142        /**
143         * @param credentials Sets credentials to the specified value.
144         */
145        public void setCredentials( Credentials credentials ) {
146            this.credentials = credentials;
147        }
148    
149        /**
150         * @return workspaceName
151         */
152        public String getWorkspaceName() {
153            return this.workspaceName;
154        }
155    
156        /**
157         * @param workspaceName Sets workspaceName to the specified value.
158         */
159        public void setWorkspaceName( String workspaceName ) {
160            this.workspaceName = workspaceName;
161        }
162    
163        /**
164         * @return pathToTopOfRepository
165         */
166        public String getPathToTopOfRepository() {
167            return this.pathToTopOfRepository;
168        }
169    
170        /**
171         * @param pathToTopOfRepository Sets pathToTopOfRepository to the specified value.
172         */
173        public void setPathToTopOfRepository( String pathToTopOfRepository ) {
174            this.pathToTopOfRepository = pathToTopOfRepository != null ? pathToTopOfRepository.trim() : DEFAULT_PATH_TO_TOP_OF_MAVEN_REPOSITORY;
175        }
176    
177        /**
178         * Get the JCR repository used by this provider
179         * 
180         * @return the repository instance
181         */
182        public Repository getRepository() {
183            return this.repository;
184        }
185    
186        /**
187         * @param repository Sets repository to the specified value.
188         */
189        public void setRepository( Repository repository ) {
190            this.repository = repository;
191        }
192    
193        /**
194         * {@inheritDoc}
195         */
196        public URL getUrl( MavenId mavenId,
197                           ArtifactType artifactType,
198                           SignatureType signatureType,
199                           boolean createIfRequired ) throws MalformedURLException, MavenRepositoryException {
200            final String path = getUrlPath(mavenId, artifactType, signatureType);
201            MavenUrl mavenUrl = new MavenUrl();
202            mavenUrl.setWorkspaceName(this.getWorkspaceName());
203            mavenUrl.setPath(path);
204            if (createIfRequired) {
205                final boolean metadataFile = ArtifactType.METADATA == artifactType;
206                final String relPath = mavenId.getRelativePath(!metadataFile);
207                Session session = null;
208                try {
209                    session = this.createSession();
210                    Node root = session.getRootNode();
211                    Node top = getOrCreatePath(root, this.getPathToTopOfRepository(), "nt:folder");
212                    session.save();
213    
214                    // Create the "nt:unstructured" nodes for the folder structures ...
215                    Node current = getOrCreatePath(top, relPath, "nt:folder");
216    
217                    // Now create the node that represents the artifact (w/ signature?) ...
218                    if (artifactType != null) {
219                        String name = metadataFile ? "" : mavenId.getArtifactId() + "-" + mavenId.getVersion();
220                        name = name + artifactType.getSuffix();
221                        if (signatureType != null) {
222                            name = name + signatureType.getSuffix();
223                        }
224                        if (current.hasNode(name)) {
225                            current = current.getNode(name);
226                        } else {
227                            // Create the node and set all of the required properties ...
228                            current = current.addNode(name, "nt:file");
229                        }
230                        if (!current.hasNode(CONTENT_NODE_NAME)) {
231                            Node contentNode = current.addNode(CONTENT_NODE_NAME, "nt:resource");
232                            contentNode.setProperty("jcr:mimeType", "text/plain");
233                            contentNode.setProperty("jcr:lastModified", Calendar.getInstance());
234                            contentNode.setProperty(CONTENT_PROPERTY_NAME, new ByteArrayInputStream("".getBytes()));
235                        }
236                    }
237                    session.save();
238                    this.logger.trace("Created Maven repository node for {0}", mavenUrl);
239                } catch (LoginException err) {
240                    throw new MavenRepositoryException(
241                                                       MavenI18n.unableToOpenSessiontoRepositoryWhenCreatingNode.text(mavenUrl,
242                                                                                                                      err.getMessage()),
243                                                       err);
244                } catch (NoSuchWorkspaceException err) {
245                    throw new MavenRepositoryException(MavenI18n.unableToFindWorkspaceWhenCreatingNode.text(this.getWorkspaceName(),
246                                                                                                            mavenUrl,
247                                                                                                            err.getMessage()), err);
248                } catch (PathNotFoundException err) {
249                    return null;
250                } catch (RepositoryException err) {
251                    throw new MavenRepositoryException(MavenI18n.errorCreatingNode.text(mavenUrl, err.getMessage()), err);
252                } finally {
253                    if (session != null) session.logout();
254                }
255            }
256            return mavenUrl.getUrl(this.urlStreamHandler, this.urlEncoder);
257        }
258    
259        protected Node getOrCreatePath( Node root,
260                                        String relPath,
261                                        String nodeType )
262            throws PathNotFoundException, ItemExistsException, NoSuchNodeTypeException, LockException, VersionException,
263            ConstraintViolationException, RepositoryException {
264            // Create the "nt:unstructured" nodes for the folder structures ...
265            Node current = root;
266            boolean created = false;
267            String[] pathComponents = relPath.replaceFirst("^/+", "").split("/");
268            for (String pathComponent : pathComponents) {
269                if (pathComponent.length() == 0) continue;
270                if (current.hasNode(pathComponent)) {
271                    current = current.getNode(pathComponent);
272                } else {
273                    current = current.addNode(pathComponent, "nt:folder");
274                    created = true;
275                }
276            }
277            if (created) {
278                this.logger.debug("Created Maven repository folders {0}", current.getPath());
279            }
280            return current;
281        }
282    
283        protected Node getContentNodeForMavenResource( Session session,
284                                                       MavenUrl mavenUrl ) throws RepositoryException {
285            final String mavenPath = mavenUrl.getPath().replaceFirst("^/+", "");
286            final String mavenRootPath = this.getPathToTopOfRepository().replaceFirst("^/+", "");
287            Node root = session.getRootNode();
288            Node top = root.getNode(mavenRootPath);
289            Node resourceNode = top.getNode(mavenPath);
290            return resourceNode.getNode(CONTENT_NODE_NAME);
291        }
292    
293        /**
294         * Get the JRC path to the node in this repository and it's workspace that represents the artifact with the given type in the
295         * supplied Maven project.
296         * 
297         * @param mavenId the ID of the Maven project; may not be null
298         * @param artifactType the type of artifact; may be null
299         * @param signatureType the type of signature; may be null if the signature file is not desired
300         * @return the path
301         */
302        protected String getUrlPath( MavenId mavenId,
303                                     ArtifactType artifactType,
304                                     SignatureType signatureType ) {
305            StringBuilder sb = new StringBuilder();
306            sb.append("/");
307            if (artifactType == null) {
308                sb.append(mavenId.getRelativePath());
309                sb.append("/");
310            } else if (ArtifactType.METADATA == artifactType) {
311                sb.append(mavenId.getRelativePath(false));
312                sb.append("/");
313            } else {
314                // Add the file in the version
315                sb.append(mavenId.getRelativePath());
316                sb.append("/");
317                sb.append(mavenId.getArtifactId());
318                sb.append("-");
319                sb.append(mavenId.getVersion());
320            }
321            if (artifactType != null) {
322                sb.append(artifactType.getSuffix());
323            }
324            if (signatureType != null) {
325                sb.append(signatureType.getSuffix());
326            }
327            return sb.toString();
328        }
329    
330        protected TextEncoder getUrlEncoder() {
331            return this.urlEncoder;
332        }
333    
334        protected TextDecoder getUrlDecoder() {
335            return this.urlDecoder;
336        }
337    
338        protected Session createSession() throws LoginException, NoSuchWorkspaceException, RepositoryException {
339            if (this.workspaceName != null) {
340                if (this.credentials != null) {
341                    return this.repository.login(this.credentials, this.workspaceName);
342                }
343                return this.repository.login(this.workspaceName);
344            }
345            if (this.credentials != null) {
346                return this.repository.login(this.credentials);
347            }
348            return this.repository.login();
349        }
350    
351        /**
352         * Obtain an input stream to the existing content at the location given by the supplied {@link MavenUrl}. The Maven URL
353         * should have a path that points to the node where the content is stored in the
354         * {@link #CONTENT_PROPERTY_NAME content property}.
355         * 
356         * @param mavenUrl the Maven URL to the content; may not be null
357         * @return the input stream to the content, or null if there is no existing content
358         * @throws IOException
359         */
360        protected InputStream getInputStream( MavenUrl mavenUrl ) throws IOException {
361            Session session = null;
362            try {
363                // Create a new session, get the actual input stream to the underlying node, and return a wrapper to the actual
364                // InputStream that, when closed, will close the session.
365                session = this.createSession();
366                // Find the node and it's property ...
367                final Node contentNode = getContentNodeForMavenResource(session, mavenUrl);
368                Property contentProperty = contentNode.getProperty(CONTENT_PROPERTY_NAME);
369                InputStream result = contentProperty.getStream();
370                result = new MavenInputStream(session, result);
371                return result;
372            } catch (LoginException err) {
373                throw new MavenRepositoryException(MavenI18n.unableToOpenSessiontoRepositoryWhenReadingNode.text(mavenUrl,
374                                                                                                                 err.getMessage()),
375                                                   err);
376            } catch (NoSuchWorkspaceException err) {
377                throw new MavenRepositoryException(MavenI18n.unableToFindWorkspaceWhenReadingNode.text(this.getWorkspaceName(),
378                                                                                                       mavenUrl,
379                                                                                                       err.getMessage()), err);
380            } catch (PathNotFoundException err) {
381                return null;
382            } catch (RepositoryException err) {
383                throw new MavenRepositoryException(MavenI18n.errorReadingNode.text(mavenUrl, err.getMessage()), err);
384            } finally {
385                if (session != null) {
386                    session.logout();
387                }
388            }
389        }
390    
391        /**
392         * Obtain an output stream to the existing content at the location given by the supplied {@link MavenUrl}. The Maven URL
393         * should have a path that points to the property or to the node where the content is stored in the
394         * {@link #CONTENT_PROPERTY_NAME content property}.
395         * 
396         * @param mavenUrl the Maven URL to the content; may not be null
397         * @return the input stream to the content, or null if there is no existing content
398         * @throws IOException
399         */
400        protected OutputStream getOutputStream( MavenUrl mavenUrl ) throws IOException {
401            try {
402                // Create a temporary file to which the content will be written and then read from ...
403                OutputStream result = null;
404                try {
405                    File tempFile = File.createTempFile("dnamaven", null);
406                    result = new MavenOutputStream(mavenUrl, tempFile);
407                } catch (IOException err) {
408                    throw new RepositoryException("Unable to obtain a temporary file for streaming content to " + mavenUrl, err);
409                }
410                return result;
411            } catch (LoginException err) {
412                throw new MavenRepositoryException(MavenI18n.unableToOpenSessiontoRepositoryWhenReadingNode.text(mavenUrl,
413                                                                                                                 err.getMessage()),
414                                                   err);
415            } catch (NoSuchWorkspaceException err) {
416                throw new MavenRepositoryException(MavenI18n.unableToFindWorkspaceWhenReadingNode.text(this.getWorkspaceName(),
417                                                                                                       mavenUrl,
418                                                                                                       err.getMessage()), err);
419            } catch (RepositoryException err) {
420                throw new MavenRepositoryException(MavenI18n.errorReadingNode.text(mavenUrl, err.getMessage()), err);
421            }
422        }
423    
424        public void setContent( MavenUrl mavenUrl,
425                                InputStream content ) throws IOException {
426            Session session = null;
427            try {
428                // Create a new session, find the actual node, create a temporary file to which the content will be written,
429                // and return a wrapper to the actual Output that writes to the file and that, when closed, will set the
430                // content on the node and close the session.
431                session = this.createSession();
432                // Find the node and it's property ...
433                final Node contentNode = getContentNodeForMavenResource(session, mavenUrl);
434                contentNode.setProperty(CONTENT_PROPERTY_NAME, content);
435                session.save();
436            } catch (LoginException err) {
437                throw new IOException(MavenI18n.unableToOpenSessiontoRepositoryWhenWritingNode.text(mavenUrl, err.getMessage()));
438            } catch (NoSuchWorkspaceException err) {
439                throw new IOException(MavenI18n.unableToFindWorkspaceWhenWritingNode.text(this.getWorkspaceName(),
440                                                                                          mavenUrl,
441                                                                                          err.getMessage()));
442            } catch (RepositoryException err) {
443                throw new IOException(MavenI18n.errorWritingNode.text(mavenUrl, err.getMessage()));
444            } finally {
445                if (session != null) {
446                    session.logout();
447                }
448            }
449        }
450    
451        protected class MavenInputStream extends InputStream {
452    
453            private final InputStream stream;
454            private final Session session;
455    
456            protected MavenInputStream( final Session session,
457                                        final InputStream stream ) {
458                this.session = session;
459                this.stream = stream;
460            }
461    
462            /**
463             * {@inheritDoc}
464             */
465            @Override
466            public int read() throws IOException {
467                return stream.read();
468            }
469    
470            /**
471             * {@inheritDoc}
472             */
473            @Override
474            public void close() throws IOException {
475                try {
476                    stream.close();
477                } finally {
478                    this.session.logout();
479                }
480            }
481        }
482    
483        protected class MavenOutputStream extends OutputStream {
484    
485            private OutputStream stream;
486            private final File file;
487            private final MavenUrl mavenUrl;
488    
489            protected MavenOutputStream( final MavenUrl mavenUrl,
490                                         final File file ) throws FileNotFoundException {
491                this.mavenUrl = mavenUrl;
492                this.file = file;
493                this.stream = new BufferedOutputStream(new FileOutputStream(this.file));
494                assert this.file != null;
495            }
496    
497            /**
498             * {@inheritDoc}
499             */
500            @Override
501            public void write( int b ) throws IOException {
502                if (stream == null) throw new IOException(MavenI18n.unableToWriteToClosedStream.text());
503                stream.write(b);
504            }
505    
506            /**
507             * {@inheritDoc}
508             */
509            @Override
510            public void write( byte[] b ) throws IOException {
511                if (stream == null) throw new IOException(MavenI18n.unableToWriteToClosedStream.text());
512                stream.write(b);
513            }
514    
515            /**
516             * {@inheritDoc}
517             */
518            @Override
519            public void write( byte[] b,
520                               int off,
521                               int len ) throws IOException {
522                if (stream == null) throw new IOException(MavenI18n.unableToWriteToClosedStream.text());
523                stream.write(b, off, len);
524            }
525    
526            /**
527             * {@inheritDoc}
528             */
529            @Override
530            public void close() throws IOException {
531                // Close the output stream to the temporary file
532                if (stream != null) {
533                    stream.close();
534                    InputStream inputStream = null;
535                    try {
536                        // Create an input stream to the temporary file...
537                        inputStream = new BufferedInputStream(new FileInputStream(file));
538                        // Write the content to the node ...
539                        setContent(this.mavenUrl, inputStream);
540    
541                    } finally {
542                        if (inputStream != null) {
543                            try {
544                                inputStream.close();
545                            } catch (IOException ioe) {
546                                Logger.getLogger(this.getClass()).error(ioe,
547                                                                        MavenI18n.errorClosingTempFileStreamAfterWritingContent,
548                                                                        mavenUrl,
549                                                                        ioe.getMessage());
550                            } finally {
551                                try {
552                                    file.delete();
553                                } catch (SecurityException se) {
554                                    Logger.getLogger(this.getClass()).error(se,
555                                                                            MavenI18n.errorDeletingTempFileStreamAfterWritingContent,
556                                                                            mavenUrl,
557                                                                            se.getMessage());
558                                } finally {
559                                    stream = null;
560                                }
561                            }
562                        }
563                    }
564                    super.close();
565                }
566            }
567        }
568    
569        /**
570         * This {@link URLStreamHandler} specialization understands {@link URL URLs} that point to content in the JCR repository used
571         * by this Maven repository.
572         * 
573         * @author Randall Hauch
574         */
575        protected class JcrUrlStreamHandler extends URLStreamHandler {
576    
577            protected JcrUrlStreamHandler() {
578            }
579    
580            /**
581             * {@inheritDoc}
582             */
583            @Override
584            protected URLConnection openConnection( URL url ) {
585                return new MavenUrlConnection(url);
586            }
587        }
588    
589        /**
590         * A URLConnection with support for obtaining content from a node in a JCR repository.
591         * <p>
592         * Each JcrUrlConnection is used to make a single request to read or write the <code>jcr:content</code> property value on
593         * the {@link javax.jcr.Node node} that corresponds to the given URL. The node must already exist.
594         * </p>
595         * 
596         * @author Randall Hauch
597         */
598        protected class MavenUrlConnection extends URLConnection {
599    
600            private final MavenUrl mavenUrl;
601    
602            /**
603             * @param url the URL that is to be processed
604             */
605            protected MavenUrlConnection( URL url ) {
606                super(url);
607                this.mavenUrl = MavenUrl.parse(url, JcrMavenUrlProvider.this.getUrlDecoder());
608            }
609    
610            /**
611             * {@inheritDoc}
612             */
613            @Override
614            public void connect() throws IOException {
615                // If the URL is not a valid JCR URL, then throw a new IOException ...
616                if (this.mavenUrl == null) {
617                    String msg = "Unable to connect to JCR repository because the URL is not valid for JCR: " + this.getURL();
618                    throw new IOException(msg);
619                }
620            }
621    
622            /**
623             * {@inheritDoc}
624             */
625            @Override
626            public InputStream getInputStream() throws IOException {
627                return JcrMavenUrlProvider.this.getInputStream(this.mavenUrl);
628            }
629    
630            /**
631             * {@inheritDoc}
632             */
633            @Override
634            public OutputStream getOutputStream() throws IOException {
635                return JcrMavenUrlProvider.this.getOutputStream(this.mavenUrl);
636            }
637        }
638    
639    }