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