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 }