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 * @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 }