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