View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.maven.spi;
25  
26  import java.io.BufferedInputStream;
27  import java.io.BufferedOutputStream;
28  import java.io.ByteArrayInputStream;
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.FileNotFoundException;
32  import java.io.FileOutputStream;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.OutputStream;
36  import java.net.MalformedURLException;
37  import java.net.URL;
38  import java.net.URLConnection;
39  import java.net.URLStreamHandler;
40  import java.util.Calendar;
41  import java.util.Properties;
42  import javax.jcr.Binary;
43  import javax.jcr.Credentials;
44  import javax.jcr.ItemExistsException;
45  import javax.jcr.LoginException;
46  import javax.jcr.NoSuchWorkspaceException;
47  import javax.jcr.Node;
48  import javax.jcr.PathNotFoundException;
49  import javax.jcr.Property;
50  import javax.jcr.Repository;
51  import javax.jcr.RepositoryException;
52  import javax.jcr.Session;
53  import javax.jcr.SimpleCredentials;
54  import javax.jcr.lock.LockException;
55  import javax.jcr.nodetype.ConstraintViolationException;
56  import javax.jcr.nodetype.NoSuchNodeTypeException;
57  import javax.jcr.version.VersionException;
58  import org.modeshape.common.text.TextDecoder;
59  import org.modeshape.common.text.TextEncoder;
60  import org.modeshape.common.text.UrlEncoder;
61  import org.modeshape.common.util.Logger;
62  import org.modeshape.maven.ArtifactType;
63  import org.modeshape.maven.MavenI18n;
64  import org.modeshape.maven.MavenId;
65  import org.modeshape.maven.MavenRepositoryException;
66  import org.modeshape.maven.MavenUrl;
67  import org.modeshape.maven.SignatureType;
68  
69  /**
70   * Base class for providers that work against a JCR repository. This class implements all functionality except for creating the
71   * {@link Repository repository} instance, and it relies upon some other component or subclass to
72   * {@link #setRepository(Repository) set the repository instance}. Typically, this is done by a subclass in it's
73   * {@link #configure(Properties)} method:
74   * 
75   * <pre>
76   * public class MyCustomJcrMavenUrlProvider extends JcrMavenUrlProvider {
77   *     &#064;Override
78   *     public void configure(Properties properties) {
79   *          super.configure(properties);
80   *          properties = super.getProperties();     // always non-null
81   *          Repository repo = ...                   // Construct and configure
82   *          super.setRepository(repo);
83   *      }
84   * }
85   * 
86   * 
87   * 
88   * </pre>
89   */
90  public class JcrMavenUrlProvider extends AbstractMavenUrlProvider {
91  
92      public static final String USERNAME = "dna.maven.urlprovider.username";
93      public static final String PASSWORD = "dna.maven.urlprovider.password";
94      public static final String WORKSPACE_NAME = "dna.maven.urlprovider.repository.workspace";
95      public static final String REPOSITORY_PATH = "dna.maven.urlprovider.repository.path";
96  
97      public static final String DEFAULT_PATH_TO_TOP_OF_MAVEN_REPOSITORY = "/dnaMavenRepository";
98      public static final String DEFAULT_CREATE_REPOSITORY_PATH = Boolean.TRUE.toString();
99  
100     public static final String CONTENT_NODE_NAME = "jcr:content";
101     public static final String CONTENT_PROPERTY_NAME = "jcr:data";
102 
103     private final URLStreamHandler urlStreamHandler = new JcrUrlStreamHandler();
104     private final TextEncoder urlEncoder;
105     private final TextDecoder urlDecoder;
106     private Repository repository;
107     private String workspaceName;
108     private Credentials credentials;
109     private String pathToTopOfRepository = DEFAULT_PATH_TO_TOP_OF_MAVEN_REPOSITORY;
110     private static final Logger LOGGER = Logger.getLogger(JcrMavenUrlProvider.class);
111 
112     /**
113      * 
114      */
115     public JcrMavenUrlProvider() {
116         UrlEncoder encoder = new UrlEncoder().setSlashEncoded(false);
117         this.urlEncoder = encoder;
118         this.urlDecoder = encoder;
119     }
120 
121     /**
122      * {@inheritDoc}
123      */
124     @Override
125     public void configure( Properties properties ) {
126         super.configure(properties);
127         properties = super.getProperties();
128         String username = properties.getProperty(USERNAME);
129         if (username != null) {
130             String password = properties.getProperty(PASSWORD, "");
131             this.setCredentials(new SimpleCredentials(username, password.toCharArray()));
132         }
133         this.setWorkspaceName(properties.getProperty(WORKSPACE_NAME, this.getWorkspaceName()));
134         this.setPathToTopOfRepository(properties.getProperty(REPOSITORY_PATH, this.getPathToTopOfRepository()));
135     }
136 
137     /**
138      * @return credentials
139      */
140     public Credentials getCredentials() {
141         return this.credentials;
142     }
143 
144     /**
145      * @param credentials Sets credentials to the specified value.
146      */
147     public void setCredentials( Credentials credentials ) {
148         this.credentials = credentials;
149     }
150 
151     /**
152      * @return workspaceName
153      */
154     public String getWorkspaceName() {
155         return this.workspaceName;
156     }
157 
158     /**
159      * @param workspaceName Sets workspaceName to the specified value.
160      */
161     public void setWorkspaceName( String workspaceName ) {
162         this.workspaceName = workspaceName;
163     }
164 
165     /**
166      * @return pathToTopOfRepository
167      */
168     public String getPathToTopOfRepository() {
169         return this.pathToTopOfRepository;
170     }
171 
172     /**
173      * @param pathToTopOfRepository Sets pathToTopOfRepository to the specified value.
174      */
175     public void setPathToTopOfRepository( String pathToTopOfRepository ) {
176         this.pathToTopOfRepository = pathToTopOfRepository != null ? pathToTopOfRepository.trim() : DEFAULT_PATH_TO_TOP_OF_MAVEN_REPOSITORY;
177     }
178 
179     /**
180      * Get the JCR repository used by this provider
181      * 
182      * @return the repository instance
183      */
184     public Repository getRepository() {
185         return this.repository;
186     }
187 
188     /**
189      * @param repository Sets repository to the specified value.
190      */
191     public void setRepository( Repository repository ) {
192         this.repository = repository;
193     }
194 
195     /**
196      * {@inheritDoc}
197      */
198     public URL getUrl( MavenId mavenId,
199                        ArtifactType artifactType,
200                        SignatureType signatureType,
201                        boolean createIfRequired ) throws MalformedURLException, MavenRepositoryException {
202         final String path = getUrlPath(mavenId, artifactType, signatureType);
203         MavenUrl mavenUrl = new MavenUrl();
204         mavenUrl.setWorkspaceName(this.getWorkspaceName());
205         mavenUrl.setPath(path);
206         if (createIfRequired) {
207             final boolean metadataFile = ArtifactType.METADATA == artifactType;
208             final String relPath = mavenId.getRelativePath(!metadataFile);
209             Session session = null;
210             try {
211                 session = this.createSession();
212                 Node root = session.getRootNode();
213                 Node top = getOrCreatePath(root, this.getPathToTopOfRepository(), "nt:folder");
214                 session.save();
215 
216                 // Create the "nt:unstructured" nodes for the folder structures ...
217                 Node current = getOrCreatePath(top, relPath, "nt:folder");
218 
219                 // Now create the node that represents the artifact (w/ signature?) ...
220                 if (artifactType != null) {
221                     String name = metadataFile ? "" : mavenId.getArtifactId() + "-" + mavenId.getVersion();
222                     name = name + artifactType.getSuffix();
223                     if (signatureType != null) {
224                         name = name + signatureType.getSuffix();
225                     }
226                     if (current.hasNode(name)) {
227                         current = current.getNode(name);
228                     } else {
229                         // Create the node and set all of the required properties ...
230                         current = current.addNode(name, "nt:file");
231                     }
232                     if (!current.hasNode(CONTENT_NODE_NAME)) {
233                         Node contentNode = current.addNode(CONTENT_NODE_NAME, "nt:resource");
234                         contentNode.setProperty("jcr:mimeType", "text/plain");
235                         contentNode.setProperty("jcr:lastModified", Calendar.getInstance());
236                         Binary binary = session.getValueFactory().createBinary(new ByteArrayInputStream("".getBytes()));
237                         contentNode.setProperty(CONTENT_PROPERTY_NAME, binary);
238                     }
239                 }
240                 session.save();
241                 LOGGER.trace("Created Maven repository node for {0}", mavenUrl);
242             } catch (LoginException err) {
243                 throw new MavenRepositoryException(
244                                                    MavenI18n.unableToOpenSessiontoRepositoryWhenCreatingNode.text(mavenUrl,
245                                                                                                                   err.getMessage()),
246                                                    err);
247             } catch (NoSuchWorkspaceException err) {
248                 throw new MavenRepositoryException(MavenI18n.unableToFindWorkspaceWhenCreatingNode.text(this.getWorkspaceName(),
249                                                                                                         mavenUrl,
250                                                                                                         err.getMessage()), err);
251             } catch (PathNotFoundException err) {
252                 return null;
253             } catch (RepositoryException err) {
254                 throw new MavenRepositoryException(MavenI18n.errorCreatingNode.text(mavenUrl, err.getMessage()), err);
255             } finally {
256                 if (session != null) session.logout();
257             }
258         }
259         return mavenUrl.getUrl(this.urlStreamHandler, this.urlEncoder);
260     }
261 
262     protected Node getOrCreatePath( Node root,
263                                     String relPath,
264                                     String nodeType )
265         throws PathNotFoundException, ItemExistsException, NoSuchNodeTypeException, LockException, VersionException,
266         ConstraintViolationException, RepositoryException {
267         // Create the "nt:unstructured" nodes for the folder structures ...
268         Node current = root;
269         boolean created = false;
270         String[] pathComponents = relPath.replaceFirst("^/+", "").split("/");
271         for (String pathComponent : pathComponents) {
272             if (pathComponent.length() == 0) continue;
273             if (current.hasNode(pathComponent)) {
274                 current = current.getNode(pathComponent);
275             } else {
276                 current = current.addNode(pathComponent, "nt:folder");
277                 created = true;
278             }
279         }
280         if (created) {
281             LOGGER.debug("Created Maven repository folders {0}", current.getPath());
282         }
283         return current;
284     }
285 
286     protected Node getContentNodeForMavenResource( Session session,
287                                                    MavenUrl mavenUrl ) throws RepositoryException {
288         final String mavenPath = mavenUrl.getPath().replaceFirst("^/+", "");
289         final String mavenRootPath = this.getPathToTopOfRepository().replaceFirst("^/+", "");
290         Node root = session.getRootNode();
291         Node top = root.getNode(mavenRootPath);
292         Node resourceNode = top.getNode(mavenPath);
293         return resourceNode.getNode(CONTENT_NODE_NAME);
294     }
295 
296     /**
297      * Get the JRC path to the node in this repository and it's workspace that represents the artifact with the given type in the
298      * supplied Maven project.
299      * 
300      * @param mavenId the ID of the Maven project; may not be null
301      * @param artifactType the type of artifact; may be null
302      * @param signatureType the type of signature; may be null if the signature file is not desired
303      * @return the path
304      */
305     protected String getUrlPath( MavenId mavenId,
306                                  ArtifactType artifactType,
307                                  SignatureType signatureType ) {
308         StringBuilder sb = new StringBuilder();
309         sb.append("/");
310         if (artifactType == null) {
311             sb.append(mavenId.getRelativePath());
312             sb.append("/");
313         } else if (ArtifactType.METADATA == artifactType) {
314             sb.append(mavenId.getRelativePath(false));
315             sb.append("/");
316         } else {
317             // Add the file in the version
318             sb.append(mavenId.getRelativePath());
319             sb.append("/");
320             sb.append(mavenId.getArtifactId());
321             sb.append("-");
322             sb.append(mavenId.getVersion());
323         }
324         if (artifactType != null) {
325             sb.append(artifactType.getSuffix());
326         }
327         if (signatureType != null) {
328             sb.append(signatureType.getSuffix());
329         }
330         return sb.toString();
331     }
332 
333     protected TextEncoder getUrlEncoder() {
334         return this.urlEncoder;
335     }
336 
337     protected TextDecoder getUrlDecoder() {
338         return this.urlDecoder;
339     }
340 
341     protected Session createSession() throws LoginException, NoSuchWorkspaceException, RepositoryException {
342         if (this.workspaceName != null) {
343             if (this.credentials != null) {
344                 return this.repository.login(this.credentials, this.workspaceName);
345             }
346             return this.repository.login(this.workspaceName);
347         }
348         if (this.credentials != null) {
349             return this.repository.login(this.credentials);
350         }
351         return this.repository.login();
352     }
353 
354     /**
355      * Obtain an input stream to the existing content at the location given by the supplied {@link MavenUrl}. The Maven URL should
356      * have a path that points to the node where the content is stored in the {@link #CONTENT_PROPERTY_NAME content property}.
357      * 
358      * @param mavenUrl the Maven URL to the content; may not be null
359      * @return the input stream to the content, or null if there is no existing content
360      * @throws IOException
361      */
362     protected InputStream getInputStream( MavenUrl mavenUrl ) throws IOException {
363         Session session = null;
364         try {
365             // Create a new session, get the actual input stream to the underlying node, and return a wrapper to the actual
366             // InputStream that, when closed, will close the session.
367             session = this.createSession();
368             // Find the node and it's property ...
369             final Node contentNode = getContentNodeForMavenResource(session, mavenUrl);
370             Property contentProperty = contentNode.getProperty(CONTENT_PROPERTY_NAME);
371             InputStream result = contentProperty.getBinary().getStream();
372             result = new MavenInputStream(session, result);
373             return result;
374         } catch (LoginException err) {
375             throw new MavenRepositoryException(MavenI18n.unableToOpenSessiontoRepositoryWhenReadingNode.text(mavenUrl,
376                                                                                                              err.getMessage()),
377                                                err);
378         } catch (NoSuchWorkspaceException err) {
379             throw new MavenRepositoryException(MavenI18n.unableToFindWorkspaceWhenReadingNode.text(this.getWorkspaceName(),
380                                                                                                    mavenUrl,
381                                                                                                    err.getMessage()), err);
382         } catch (PathNotFoundException err) {
383             return null;
384         } catch (RepositoryException err) {
385             throw new MavenRepositoryException(MavenI18n.errorReadingNode.text(mavenUrl, err.getMessage()), err);
386         } finally {
387             if (session != null) {
388                 session.logout();
389             }
390         }
391     }
392 
393     /**
394      * Obtain an output stream to the existing content at the location given by the supplied {@link MavenUrl}. The Maven URL
395      * should have a path that points to the property or to the node where the content is stored in the
396      * {@link #CONTENT_PROPERTY_NAME content property}.
397      * 
398      * @param mavenUrl the Maven URL to the content; may not be null
399      * @return the input stream to the content, or null if there is no existing content
400      * @throws IOException
401      */
402     protected OutputStream getOutputStream( MavenUrl mavenUrl ) throws IOException {
403         try {
404             // Create a temporary file to which the content will be written and then read from ...
405             OutputStream result = null;
406             try {
407                 File tempFile = File.createTempFile("dnamaven", null);
408                 result = new MavenOutputStream(mavenUrl, tempFile);
409             } catch (IOException err) {
410                 throw new RepositoryException("Unable to obtain a temporary file for streaming content to " + mavenUrl, err);
411             }
412             return result;
413         } catch (LoginException err) {
414             throw new MavenRepositoryException(MavenI18n.unableToOpenSessiontoRepositoryWhenReadingNode.text(mavenUrl,
415                                                                                                              err.getMessage()),
416                                                err);
417         } catch (NoSuchWorkspaceException err) {
418             throw new MavenRepositoryException(MavenI18n.unableToFindWorkspaceWhenReadingNode.text(this.getWorkspaceName(),
419                                                                                                    mavenUrl,
420                                                                                                    err.getMessage()), err);
421         } catch (RepositoryException err) {
422             throw new MavenRepositoryException(MavenI18n.errorReadingNode.text(mavenUrl, err.getMessage()), err);
423         }
424     }
425 
426     public void setContent( MavenUrl mavenUrl,
427                             InputStream content ) throws IOException {
428         Session session = null;
429         try {
430             // Create a new session, find the actual node, create a temporary file to which the content will be written,
431             // and return a wrapper to the actual Output that writes to the file and that, when closed, will set the
432             // content on the node and close the session.
433             session = this.createSession();
434             // Find the node and it's property ...
435             final Node contentNode = getContentNodeForMavenResource(session, mavenUrl);
436             contentNode.setProperty(CONTENT_PROPERTY_NAME, session.getValueFactory().createBinary(content));
437             session.save();
438         } catch (LoginException err) {
439             throw new IOException(MavenI18n.unableToOpenSessiontoRepositoryWhenWritingNode.text(mavenUrl, err.getMessage()));
440         } catch (NoSuchWorkspaceException err) {
441             throw new IOException(MavenI18n.unableToFindWorkspaceWhenWritingNode.text(this.getWorkspaceName(),
442                                                                                       mavenUrl,
443                                                                                       err.getMessage()));
444         } catch (RepositoryException err) {
445             throw new IOException(MavenI18n.errorWritingNode.text(mavenUrl, err.getMessage()));
446         } finally {
447             if (session != null) {
448                 session.logout();
449             }
450         }
451     }
452 
453     protected class MavenInputStream extends InputStream {
454 
455         private final InputStream stream;
456         private final Session session;
457 
458         protected MavenInputStream( final Session session,
459                                     final InputStream stream ) {
460             this.session = session;
461             this.stream = stream;
462         }
463 
464         /**
465          * {@inheritDoc}
466          */
467         @Override
468         public int read() throws IOException {
469             return stream.read();
470         }
471 
472         /**
473          * {@inheritDoc}
474          */
475         @Override
476         public void close() throws IOException {
477             try {
478                 stream.close();
479             } finally {
480                 this.session.logout();
481             }
482         }
483     }
484 
485     protected class MavenOutputStream extends OutputStream {
486 
487         private OutputStream stream;
488         private final File file;
489         private final MavenUrl mavenUrl;
490         private final Logger LOGGER = Logger.getLogger(MavenOutputStream.class);
491 
492         protected MavenOutputStream( final MavenUrl mavenUrl,
493                                      final File file ) throws FileNotFoundException {
494             this.mavenUrl = mavenUrl;
495             this.file = file;
496             this.stream = new BufferedOutputStream(new FileOutputStream(this.file));
497             assert this.file != null;
498         }
499 
500         /**
501          * {@inheritDoc}
502          */
503         @Override
504         public void write( int b ) throws IOException {
505             if (stream == null) throw new IOException(MavenI18n.unableToWriteToClosedStream.text());
506             stream.write(b);
507         }
508 
509         /**
510          * {@inheritDoc}
511          */
512         @Override
513         public void write( byte[] b ) throws IOException {
514             if (stream == null) throw new IOException(MavenI18n.unableToWriteToClosedStream.text());
515             stream.write(b);
516         }
517 
518         /**
519          * {@inheritDoc}
520          */
521         @Override
522         public void write( byte[] b,
523                            int off,
524                            int len ) throws IOException {
525             if (stream == null) throw new IOException(MavenI18n.unableToWriteToClosedStream.text());
526             stream.write(b, off, len);
527         }
528 
529         /**
530          * {@inheritDoc}
531          */
532         @Override
533         public void close() throws IOException {
534             // Close the output stream to the temporary file
535             if (stream != null) {
536                 stream.close();
537                 InputStream inputStream = null;
538                 try {
539                     // Create an input stream to the temporary file...
540                     inputStream = new BufferedInputStream(new FileInputStream(file));
541                     // Write the content to the node ...
542                     setContent(this.mavenUrl, inputStream);
543 
544                 } finally {
545                     if (inputStream != null) {
546                         try {
547                             inputStream.close();
548                         } catch (IOException ioe) {
549                             LOGGER.error(ioe, MavenI18n.errorClosingTempFileStreamAfterWritingContent, mavenUrl, 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 the
593      * {@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 }