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 }